gandi.cli-1.2/0000755000175000017500000000000013227415174014070 5ustar sayounsayoun00000000000000gandi.cli-1.2/LICENSE0000644000175000017500000010451312441335654015102 0ustar sayounsayoun00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the 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 . 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: Copyright (C) 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 . 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 . gandi.cli-1.2/gandi/0000755000175000017500000000000013227415174015152 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/0000755000175000017500000000000013227415174015721 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/modules/0000755000175000017500000000000013227415174017371 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/modules/snapshotprofile.py0000644000175000017500000000422012576313413023160 0ustar sayounsayoun00000000000000""" Snapshot profile commands module. """ from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import DuplicateResults class SnapshotProfile(GandiModule): """ Module to handle CLI commands. $ gandi snapshotprofile info $ gandi snapshotprofile list """ @classmethod def from_name(cls, name): """ Retrieve a snapshot profile accsociated to a name.""" snps = cls.list({'name': name}) if len(snps) == 1: return snps[0]['id'] elif not snps: return raise DuplicateResults('snapshot profile name %s is ambiguous.' % name) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be name or id.""" try: qry_id = cls.from_name(id) if not qry_id: qry_id = int(id) except DuplicateResults as exc: cls.error(exc.errors) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def list(cls, options=None, target=None): """ List all snapshot profiles.""" options = options or {} result = [] if not target or target == 'paas': for profile in cls.safe_call('paas.snapshotprofile.list', options): profile['target'] = 'paas' result.append((profile['id'], profile)) if not target or target == 'vm': for profile in cls.safe_call('hosting.snapshotprofile.list', options): profile['target'] = 'vm' result.append((profile['id'], profile)) result = sorted(result, key=lambda item: item[0]) return [profile for id_, profile in result] @classmethod def info(cls, resource): """Display information about a snapshot profile.""" snps = cls.list({'id': cls.usable_id(resource)}) if len(snps) == 1: return snps[0] elif not snps: return raise DuplicateResults('snapshot profile %s is ambiguous.' % resource) gandi.cli-1.2/gandi/cli/modules/disk.py0000644000175000017500000002105213203325477020675 0ustar sayounsayoun00000000000000""" Disk commands module. """ from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import DuplicateResults from gandi.cli.core.params import DISK_MAXLIST from .iaas import Iaas, Datacenter, Image class Disk(GandiModule): """ Module to handle CLI commands. $ gandi disk create $ gandi disk delete $ gandi disk attach $ gandi disk detach $ gandi disk info $ gandi disk list $ gandi disk rollback $ gandi disk snapshot $ gandi disk update """ @classmethod def from_name(cls, name): """ Retrieve a disk id associated to a name. """ disks = cls.list({'name': name}) if len(disks) == 1: return disks[0]['id'] elif not disks: return raise DuplicateResults('disk name %s is ambiguous.' % name) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be name or id.""" try: qry_id = cls.from_name(id) if not qry_id: qry_id = int(id) except DuplicateResults as exc: cls.error(exc.errors) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def list(cls, options=None): """ List all disks.""" options = options or {} return cls.call('hosting.disk.list', options) @classmethod def list_create(cls, datacenter=None, label=None): """List available disks for vm creation.""" options = { 'items_per_page': DISK_MAXLIST } if datacenter: datacenter_id = int(Datacenter.usable_id(datacenter)) options['datacenter_id'] = datacenter_id # implement a filter by label as API doesn't handle it images = cls.safe_call('hosting.disk.list', options) if not label: return images return [img for img in images if label.lower() in img['name'].lower()] @classmethod def _info(cls, disk_id): """ Get information about a disk.""" return cls.call('hosting.disk.info', disk_id) @classmethod def info(cls, name): """ Get information about a disk.""" return cls._info(cls.usable_id(name)) @staticmethod def disk_param(name, size, snapshot_profile, cmdline=None, kernel=None): """ Return disk parameter structure. """ disk_params = {} if cmdline: disk_params['cmdline'] = cmdline if kernel: disk_params['kernel'] = kernel if name: disk_params['name'] = name if snapshot_profile is not None: disk_params['snapshot_profile'] = snapshot_profile if size: disk_params['size'] = size return disk_params @classmethod def update(cls, resource, name, size, snapshot_profile, background, cmdline=None, kernel=None): """ Update this disk. """ if isinstance(size, tuple): prefix, size = size if prefix == '+': disk_info = cls.info(resource) current_size = disk_info['size'] size = current_size + size disk_params = cls.disk_param(name, size, snapshot_profile, cmdline, kernel) result = cls.call('hosting.disk.update', cls.usable_id(resource), disk_params) if background: return result # interactive mode, run a progress bar cls.echo('Updating your disk.') cls.display_progress(result) @classmethod def _detach(cls, disk_id): """ Detach a disk from a vm. """ disk = cls._info(disk_id) opers = [] if disk.get('vms_id'): for vm_id in disk['vms_id']: cls.echo('The disk is still attached to the vm %s.' % vm_id) cls.echo('Will detach it.') opers.append(cls.call('hosting.vm.disk_detach', vm_id, disk_id)) return opers @classmethod def detach(cls, resources, background): if not isinstance(resources, (list, tuple)): resources = [resources] resources = [cls.usable_id(item) for item in resources] opers = [] for disk_id in resources: opers.extend(cls._detach(disk_id)) if opers and not background: cls.echo('Detaching your disk(s).') cls.display_progress(opers) return opers @classmethod def delete(cls, resources, background=False): """ Delete this disk.""" if not isinstance(resources, (list, tuple)): resources = [resources] resources = [cls.usable_id(item) for item in resources] opers = [] for disk_id in resources: opers.extend(cls._detach(disk_id)) if opers: cls.echo('Detaching your disk(s).') cls.display_progress(opers) opers = [] for disk_id in resources: oper = cls.call('hosting.disk.delete', disk_id) opers.append(oper) if background: return opers cls.echo('Deleting your disk.') cls.display_progress(opers) return opers @classmethod def _attach(cls, disk_id, vm_id, options=None): """ Attach a disk to a vm. """ options = options or {} oper = cls.call('hosting.vm.disk_attach', vm_id, disk_id, options) return oper @classmethod def attach(cls, disk, vm, background, position=None, read_only=False): from gandi.cli.modules.iaas import Iaas as VM vm_id = VM.usable_id(vm) disk_id = cls.usable_id(disk) disk_info = cls._info(disk_id) options = {} if position is not None: options['position'] = position if read_only: options['access'] = 'read' need_detach = disk_info.get('vms_id') if need_detach: if disk_info.get('vms_id') == [vm_id]: cls.echo('This disk is already attached to this vm.') return # detach disk detach_op = cls._detach(disk_id) # interactive mode, run a progress bar cls.echo('Detaching your disk.') cls.display_progress(detach_op) oper = cls._attach(disk_id, vm_id, options) if oper and not background: cls.echo('Attaching your disk(s).') cls.display_progress(oper) return oper @classmethod def create(cls, name, vm, size, snapshotprofile, datacenter, source, disk_type='data', background=False): """ Create a disk and attach it to a vm. """ if isinstance(size, tuple): prefix, size = size if source: size = None disk_params = cls.disk_param(name, size, snapshotprofile) disk_params['datacenter_id'] = int(Datacenter.usable_id(datacenter)) disk_params['type'] = disk_type if source: disk_id = int(Image.usable_id(source, disk_params['datacenter_id'])) result = cls.call('hosting.disk.create_from', disk_params, disk_id) else: result = cls.call('hosting.disk.create', disk_params) if background and not vm: return result # interactive mode, run a progress bar cls.echo('Creating your disk.') cls.display_progress(result) if not vm: return vm_id = Iaas.usable_id(vm) result = cls._attach(result['disk_id'], vm_id) if background: return result cls.echo('Attaching your disk.') cls.display_progress(result) @classmethod def rollback(cls, resource, background=False): """ Rollback a disk from a snapshot. """ disk_id = cls.usable_id(resource) result = cls.call('hosting.disk.rollback_from', disk_id) if background: return result cls.echo('Disk rollback in progress.') cls.display_progress(result) return result @classmethod def migrate(cls, resource, datacenter_id, background=False): """ Migrate a disk to another datacenter. """ disk_id = cls.usable_id(resource) result = cls.call('hosting.disk.migrate', disk_id, datacenter_id) if background: return result cls.echo('Disk migration in progress.') cls.display_progress(result) return result gandi.cli-1.2/gandi/cli/modules/docker.py0000644000175000017500000000232012623134755021211 0ustar sayounsayoun00000000000000import os import subprocess from gandi.cli.core.base import GandiModule from gandi.cli.modules.iaas import Iaas from gandi.cli.core.utils import unixpipe class Docker(GandiModule): """ Module to handle docker vms. $ gandi docker create $ gandi docker help $ gandi docker ps $ gandi docker Note that you can use a per-project docker vm by using a local directory gandi configuration using: $ gandi config set dockervm foobar Or override the current global vm using: $ gandi docker --vm bar ps """ @classmethod def handle(cls, vm, args): """ Setup forwarding connection to given VM and pipe docker cmds over SSH. """ docker = Iaas.info(vm) if not docker: raise Exception('docker vm %s not found' % vm) if docker['state'] != 'running': Iaas.start(vm) # XXX remote_addr = docker['ifaces'][0]['ips'][0]['ip'] port = unixpipe.setup(remote_addr, 'root', '/var/run/docker.sock') os.environ['DOCKER_HOST'] = 'tcp://localhost:%d' % port cls.echo('using DOCKER_HOST=%s' % os.environ['DOCKER_HOST']) subprocess.call(['docker'] + list(args)) gandi.cli-1.2/gandi/cli/modules/api.py0000644000175000017500000000043712441335654020521 0ustar sayounsayoun00000000000000""" API commands module. """ from gandi.cli.core.base import GandiModule class Api(GandiModule): """ Module to handle CLI commands. $ gandi api """ @classmethod def info(cls): """Display information about API.""" return cls.call('version.info') gandi.cli-1.2/gandi/cli/modules/datacenter.py0000644000175000017500000001047513155510475022064 0ustar sayounsayoun00000000000000""" Datacenter commands module. """ from __future__ import print_function from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import DatacenterClosed, DatacenterLimited class Datacenter(GandiModule): """ Module to handle CLI commands. $ gandi datacenters """ @classmethod def list(cls, options=None): """List available datacenters.""" return cls.safe_call('hosting.datacenter.list', options or {}) @classmethod def list_migration_choice(cls, datacenter): """List available datacenters for migration from given datacenter.""" datacenter_id = cls.usable_id(datacenter) dc_list = cls.list() available_dcs = [dc for dc in dc_list if dc['id'] == datacenter_id][0]['can_migrate_to'] choices = [dc for dc in dc_list if dc['id'] in available_dcs] return choices @classmethod def is_opened(cls, dc_code, type_): """List opened datacenters for given type.""" options = {'dc_code': dc_code, '%s_opened' % type_: True} datacenters = cls.safe_call('hosting.datacenter.list', options) if not datacenters: # try with ISO code options = {'iso': dc_code, '%s_opened' % type_: True} datacenters = cls.safe_call('hosting.datacenter.list', options) if not datacenters: raise DatacenterClosed(r'/!\ Datacenter %s is closed, please ' 'choose another datacenter.' % dc_code) datacenter = datacenters[0] if datacenter.get('%s_closed_for' % type_) == 'NEW': dc_close_date = datacenter.get('deactivate_at', '') if dc_close_date: dc_close_date = dc_close_date.strftime('%d/%m/%Y') raise DatacenterLimited(dc_close_date) @classmethod def filtered_list(cls, name=None, obj=None): """List datacenters matching name and compatible with obj""" options = {} if name: options['id'] = cls.usable_id(name) def obj_ok(dc, obj): if not obj or obj['datacenter_id'] == dc['id']: return True return False return [x for x in cls.list(options) if obj_ok(x, obj)] @classmethod def from_iso(cls, iso): """Retrieve the first datacenter id associated to an ISO.""" result = cls.list({'sort_by': 'id ASC'}) dc_isos = {} for dc in result: if dc['iso'] not in dc_isos: dc_isos[dc['iso']] = dc['id'] return dc_isos.get(iso) @classmethod def from_name(cls, name): """Retrieve datacenter id associated to a name.""" result = cls.list() dc_names = {} for dc in result: dc_names[dc['name']] = dc['id'] return dc_names.get(name) @classmethod def from_country(cls, country): """Retrieve the first datacenter id associated to a country.""" result = cls.list({'sort_by': 'id ASC'}) dc_countries = {} for dc in result: if dc['country'] not in dc_countries: dc_countries[dc['country']] = dc['id'] return dc_countries.get(country) @classmethod def from_dc_code(cls, dc_code): """Retrieve the datacenter id associated to a dc_code""" result = cls.list() dc_codes = {} for dc in result: if dc.get('dc_code'): dc_codes[dc['dc_code']] = dc['id'] return dc_codes.get(dc_code) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be ISO, name, country, dc_code.""" try: # id is maybe a dc_code qry_id = cls.from_dc_code(id) if not qry_id: # id is maybe a ISO qry_id = cls.from_iso(id) if qry_id: cls.deprecated('ISO code for datacenter filter use ' 'dc_code instead') if not qry_id: # id is maybe a country qry_id = cls.from_country(id) if not qry_id: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id gandi.cli-1.2/gandi/cli/modules/record.py0000644000175000017500000001013313227414205021211 0ustar sayounsayoun00000000000000""" Record commands module. """ from gandi.cli.core.base import GandiModule class Zone(GandiModule): """ Helper class for domain DNS zones. """ @classmethod def new(cls, zone_id): """Create a new zone version.""" return cls.call('domain.zone.version.new', zone_id) @classmethod def set(cls, zone_id, version_id): """Set active version of a zone.""" return cls.call('domain.zone.version.set', zone_id, version_id) @classmethod def delete(cls, zone_id, version_id): """Delete a version of a zone.""" return cls.call('domain.zone.version.delete', zone_id, version_id) class Record(GandiModule): """ Module to handle CLI commands. $ gandi record list $ gandi record create """ @classmethod def list(cls, zone_id, options=None): """List zone records for a zone.""" options = options if options else {} return cls.call('domain.zone.record.list', zone_id, 0, options) @classmethod def add(cls, zone_id, version_id, record): """Add record to a zone.""" return cls.call('domain.zone.record.add', zone_id, version_id, record) @classmethod def create(cls, zone_id, record): """Create a new zone version for record.""" cls.echo('Creating new zone version') new_version_id = Zone.new(zone_id) cls.echo('Updating zone version') cls.add(zone_id, new_version_id, record) cls.echo('Activation of new zone version') Zone.set(zone_id, new_version_id) return new_version_id @classmethod def delete(cls, zone_id, record): """Delete a record for a zone""" cls.echo('Creating new zone record') new_version_id = Zone.new(zone_id) cls.echo('Deleting zone record') cls.call('domain.zone.record.delete', zone_id, new_version_id, record) cls.echo('Activation of new zone version') Zone.set(zone_id, new_version_id) return new_version_id @classmethod def zone_update(cls, zone_id, records): """Update records for a zone""" cls.echo('Creating new zone file') new_version_id = Zone.new(zone_id) cls.echo('Updating zone records') cls.call('domain.zone.record.set', zone_id, new_version_id, records) cls.echo('Activation of new zone version') Zone.set(zone_id, new_version_id) return new_version_id @classmethod def update(cls, zone_id, old_record, new_record): """Update a record in a zone file""" cls.echo('Creating new zone file') new_version_id = Zone.new(zone_id) new_record = new_record.replace(' IN', '') new_record = new_record.split(' ', 4) params_newrecord = {'name': new_record[0], 'ttl': int(new_record[1]), 'type': new_record[2], 'value': new_record[3]} old_record = old_record.replace(' IN', '') old_record = old_record.split(' ', 4) try: params = {'name': old_record[0], 'ttl': int(old_record[1]), 'type': old_record[2], 'value': old_record[3]} except IndexError: # failed to retrieve all values, try only use the name params = {'name': old_record[0]} record = cls.call('domain.zone.record.list', zone_id, new_version_id, params) if record: cls.echo('Updating zone records') try: cls.call('domain.zone.record.update', zone_id, new_version_id, {'id': record[0]['id']}, params_newrecord) cls.echo('Activation of new zone version') Zone.set(zone_id, new_version_id) return new_version_id except Exception as err: cls.echo('An error as occured: %s' % err) Zone.delete(zone_id, new_version_id) else: cls.echo('The record to update does not exist. Check records' ' already created with `gandi record list example.com' ' --output`') return False gandi.cli-1.2/gandi/cli/modules/oper.py0000644000175000017500000000112712623134755020713 0ustar sayounsayoun00000000000000""" Operation commands module. """ from gandi.cli.core.base import GandiModule class Oper(GandiModule): """ Module to handle CLI commands. $ gandi oper info $ gandi oper list """ @classmethod def list(cls, options): """List operation.""" return cls.call('operation.list', options) @classmethod def count(cls, options): """Count operation.""" return cls.call('operation.count', options) @classmethod def info(cls, id): """Display information about an operation.""" return cls.call('operation.info', id) gandi.cli-1.2/gandi/cli/modules/domain.py0000644000175000017500000000734412714035303021212 0ustar sayounsayoun00000000000000""" Domain commands module. """ import time from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import DomainNotAvailable class Domain(GandiModule): """ Module to handle CLI commands. $ gandi domain create $ gandi domain info $ gandi domain list """ @classmethod def list(cls, options): """List operation.""" return cls.call('domain.list', options) @classmethod def info(cls, fqdn): """Display information about a domain.""" return cls.call('domain.info', fqdn) @classmethod def create(cls, fqdn, duration, owner, admin, tech, bill, nameserver, background): """Create a domain.""" fqdn = fqdn.lower() if not background and not cls.intty(): background = True result = cls.call('domain.available', [fqdn]) while result[fqdn] == 'pending': time.sleep(1) result = cls.call('domain.available', [fqdn]) if result[fqdn] == 'unavailable': raise DomainNotAvailable('%s is not available' % fqdn) # retrieve handle of user and save it to configuration user_handle = cls.call('contact.info')['handle'] cls.configure(True, 'api.handle', user_handle) owner_ = owner or user_handle admin_ = admin or user_handle tech_ = tech or user_handle bill_ = bill or user_handle domain_params = { 'duration': duration, 'owner': owner_, 'admin': admin_, 'tech': tech_, 'bill': bill_, } if nameserver: domain_params['nameservers'] = nameserver result = cls.call('domain.create', fqdn, domain_params) if background: return result # interactive mode, run a progress bar cls.echo('Creating your domain.') cls.display_progress(result) cls.echo('Your domain %s has been created.' % fqdn) @classmethod def renew(cls, fqdn, duration, background): """Renew a domain.""" fqdn = fqdn.lower() if not background and not cls.intty(): background = True domain_info = cls.info(fqdn) current_year = domain_info['date_registry_end'].year domain_params = { 'duration': duration, 'current_year': current_year, } result = cls.call('domain.renew', fqdn, domain_params) if background: return result # interactive mode, run a progress bar cls.echo('Renewing your domain.') cls.display_progress(result) cls.echo('Your domain %s has been renewed.' % fqdn) @classmethod def autorenew_deactivate(cls, fqdn): """Activate deautorenew""" fqdn = fqdn.lower() result = cls.call('domain.autorenew.deactivate', fqdn) return result @classmethod def autorenew_activate(cls, fqdn): """Activate autorenew""" fqdn = fqdn.lower() result = cls.call('domain.autorenew.activate', fqdn) return result @classmethod def from_fqdn(cls, fqdn): """Retrieve domain id associated to a FQDN.""" result = cls.list({'fqdn': fqdn}) if len(result) > 0: return result[0]['id'] @classmethod def usable_id(cls, id): """Retrieve id from input which can be fqdn or id.""" # Check if it's already an integer. try: qry_id = int(id) except Exception: # Otherwise, assume it's a FQDN. # This will return `None` if the FQDN is not found. qry_id = cls.from_fqdn(id) if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id gandi.cli-1.2/gandi/cli/modules/forward.py0000644000175000017500000000423412656121545021413 0ustar sayounsayoun00000000000000""" Forward commands module. """ from gandi.cli.core.base import GandiModule class Forward(GandiModule): """ Module to handle CLI commands. $ gandi forward create $ gandi forward delete $ gandi forward list $ gandi forward update """ @classmethod def list(cls, domain, options): """List forwards for a given domain name.""" return cls.call('domain.forward.list', domain, options) @classmethod def delete(cls, domain, source): """Delete a domain mail forward.""" return cls.call('domain.forward.delete', domain, source) @classmethod def create(cls, domain, source, destinations): """Create a domain mail forward.""" cls.echo('Creating mail forward %s@%s' % (source, domain)) options = {'destinations': list(destinations)} result = cls.call('domain.forward.create', domain, source, options) return result @classmethod def get_destinations(cls, domain, source): """Retrieve forward information.""" forwards = cls.list(domain, {'items_per_page': 500}) for fwd in forwards: if fwd['source'] == source: return fwd['destinations'] return [] @classmethod def update(cls, domain, source, dest_add, dest_del): """Update a domain mail forward destinations.""" result = None if dest_add or dest_del: current_destinations = cls.get_destinations(domain, source) fwds = current_destinations[:] if dest_add: for dest in dest_add: if dest not in fwds: fwds.append(dest) if dest_del: for dest in dest_del: if dest in fwds: fwds.remove(dest) if ((len(current_destinations) != len(fwds)) or (current_destinations != fwds)): cls.echo('Updating mail forward %s@%s' % (source, domain)) options = {'destinations': fwds} result = cls.call('domain.forward.update', domain, source, options) return result gandi.cli-1.2/gandi/cli/modules/contact.py0000644000175000017500000000142212746404125021374 0ustar sayounsayoun00000000000000""" Contact commands module. """ from gandi.cli.core.base import GandiModule class Contact(GandiModule): """ Module to handle CLI commands.""" @classmethod def info(cls): """Display information about a Contact.""" return cls.call('contact.info') @classmethod def create(cls, params): """Create a new contact.""" return cls.call('contact.create', params, empty_key=True) @classmethod def create_dry_run(cls, params): """Create a new contact.""" return cls.call('contact.create', dict(params), empty_key=True, dry_run=True, return_dry_run=True) @classmethod def balance(cls): """Retrieve balance status for a Contact.""" return cls.call('contact.balance') gandi.cli-1.2/gandi/cli/modules/status.py0000644000175000017500000000346713160664756021310 0ustar sayounsayoun00000000000000""" Status commands module. """ try: import urllib.parse as uparse except ImportError: import urllib as uparse from gandi.cli.core.base import GandiModule class Status(GandiModule): """ Module to handle CLI commands. $ gandi status """ base_url = 'https://status.gandi.net' api_url = 'https://status.gandi.net/api' @classmethod def descriptions(cls): """ Retrieve status descriptions from status.gandi.net. """ schema = cls.json_get('%s/status/schema' % cls.api_url, empty_key=True, send_key=False) descs = {} for val in schema['fields']['status']['value']: descs.update(val) return descs @classmethod def services(cls): """Retrieve services statuses from status.gandi.net.""" return cls.json_get('%s/services' % cls.api_url, empty_key=True, send_key=False) @classmethod def status(cls): """Retrieve global status from status.gandi.net.""" return cls.json_get('%s/status' % cls.api_url, empty_key=True, send_key=False) @classmethod def events(cls, filters): """Retrieve events details from status.gandi.net.""" current = filters.pop('current', False) current_params = [] if current: current_params = [('current', 'true')] filter_url = uparse.urlencode(sorted(list(filters.items())) + current_params) # noqa events = cls.json_get('%s/events?%s' % (cls.api_url, filter_url), empty_key=True, send_key=False) return events @classmethod def event_timeline(cls, event): """Retrieve event timeline url for status.gandi.net.""" return '%s/timeline/events/%s' % (cls.base_url, event['id']) gandi.cli-1.2/gandi/cli/modules/dns.py0000644000175000017500000001055613227142762020536 0ustar sayounsayoun00000000000000""" LiveDNS commands module. """ import json from gandi.cli.core.base import GandiModule class Dns(GandiModule): """ Module to handle CLI commands. $ gandi dns create $ gandi dns delete $ gandi dns domain.list $ gandi dns list $ gandi dns keys create $ gandi dns keys delete $ gandi dns keys info $ gandi dns keys list $ gandi dns keys recover """ api_url = 'https://dns.api.gandi.net/api/v5' @classmethod def get_sort_url(cls, url, sort_by=None): if sort_by: if not sort_by.startswith('rrset'): sort_key = 'rrset_%s' % sort_by else: sort_key = sort_by url = '%s?sort_by=%s' % (url, sort_key) return url @classmethod def list(cls): """List domains.""" return cls.json_get('%s/domains' % cls.api_url) @classmethod def type_list(cls): """List supported records type.""" return cls.json_get('%s/dns/rrtypes' % cls.api_url) @classmethod def get_fqdn_info(cls, fqdn): """Retrieve information about a domain""" return cls.json_get('%s/domains/%s' % (cls.api_url, fqdn)) @classmethod def records(cls, fqdn, sort_by=None, text=False): """Display records information about a domain.""" meta = cls.get_fqdn_info(fqdn) url = meta['domain_records_href'] kwargs = {} if text: kwargs = {'headers': {'Accept': 'text/plain'}} return cls.json_get(cls.get_sort_url(url, sort_by), **kwargs) @classmethod def add_record(cls, fqdn, name, type, value, ttl): """Create record for a domain.""" data = { "rrset_name": name, "rrset_type": type, "rrset_values": value, } if ttl: data['rrset_ttl'] = int(ttl) meta = cls.get_fqdn_info(fqdn) url = meta['domain_records_href'] return cls.json_post(url, data=json.dumps(data)) @classmethod def update_record(cls, fqdn, name, type, value, ttl, content): """Update all records for a domain.""" data = { "rrset_name": name, "rrset_type": type, "rrset_values": value, } if ttl: data['rrset_ttl'] = int(ttl) meta = cls.get_fqdn_info(fqdn) if content: url = meta['domain_records_href'] kwargs = {'headers': {'Content-Type': 'text/plain'}, 'data': content} return cls.json_put(url, **kwargs) url = '%s/domains/%s/records/%s/%s' % (cls.api_url, fqdn, name, type) return cls.json_put(url, data=json.dumps(data)) @classmethod def del_record(cls, fqdn, name, type): """Delete record for a domain.""" meta = cls.get_fqdn_info(fqdn) url = meta['domain_records_href'] delete_url = url if name: delete_url = '%s/%s' % (delete_url, name) if type: delete_url = '%s/%s' % (delete_url, type) return cls.json_delete(delete_url) @classmethod def keys(cls, fqdn, sort_by=None): """Display keys information about a domain.""" meta = cls.get_fqdn_info(fqdn) url = meta['domain_keys_href'] return cls.json_get(cls.get_sort_url(url, sort_by)) @classmethod def keys_info(cls, fqdn, key): """Retrieve key information.""" return cls.json_get('%s/domains/%s/keys/%s' % (cls.api_url, fqdn, key)) @classmethod def keys_create(cls, fqdn, flag): """Create new key entry for a domain.""" data = { "flags": flag, } meta = cls.get_fqdn_info(fqdn) url = meta['domain_keys_href'] ret, headers = cls.json_post(url, data=json.dumps(data), return_header=True) return cls.json_get(headers['location']) @classmethod def keys_delete(cls, fqdn, key): """Delete a key for a domain.""" return cls.json_delete('%s/domains/%s/keys/%s' % (cls.api_url, fqdn, key)) @classmethod def keys_recover(cls, fqdn, key): """Recover deleted key for a domain.""" data = { "deleted": False, } return cls.json_put('%s/domains/%s/keys/%s' % (cls.api_url, fqdn, key), data=json.dumps(data),) gandi.cli-1.2/gandi/cli/modules/cert.py0000644000175000017500000003446313117771234020712 0ustar sayounsayoun00000000000000""" Certificate commands module. """ import os import re from click import UsageError from gandi.cli.core.base import GandiModule CROSSED_PEM = '''-----BEGIN CERTIFICATE----- MIIFdzCCBF+gAwIBAgIQE+oocFv07O0MNmMJgGFDNjANBgkqhkiG9w0BAQwFADBv MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00yt UINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NC tnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQf jtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM 8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hm AUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiV Z4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9 N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sF qV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9 HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ +gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyX HAc/DVL17e8vgg8CAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTv A73gJMtUGjAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/ BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1Ud HwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4 dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0 dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAJNl9jeD lQ9ew4IcH9Z35zyKwKoJ8OkLJvHgwmp1ocd5yblSYMgpEg7wrQPWCcR23+WmgZWn RtqCV6mVksW2jwMibDN3wXsyF24HzloUQToFJBv2FAY7qCUkDrvMKnXduXBBP3zQ YzYhBx9G/2CkkeFnvN4ffhkUyWNnkepnB2u0j4vAbkN9w6GAbLIevFOFfdyQoaS8 Le9Gclc1Bb+7RrtubTeZtv8jkpHGbkD4jylW6l/VXxRTrPBPYer3IsynVgviuDQf Jtl7GQVoP7o81DgGotPmjw7jtHFtQELFhLRAlSv0ZaBIefYdgWOWnU914Ph85I6p 0fKtirOMxyHNwu8= -----END CERTIFICATE-----''' URLS = { 1: { 'std': { 'default': { 'der': 'http://crt.gandi.net/GandiStandardSSLCA.crt', 'pem': 'http://www.gandi.net/static/CAs/GandiStandardSSLCA.pem', }, }, 'pro': { 'sgc': { 'pem': 'http://www.gandi.net/static/CAs/GandiSGCSSLCA.pem', }, 'default': { 'der': 'http://crt.gandi.net/GandiProSSLCA.crt', 'pem': 'http://www.gandi.net/static/CAs/GandiProSSLCA.pem', }, }, }, 2: { 'std': { 'default': { 'der': ['http://crt.gandi.net/GandiStandardSSLCA2.crt', 'http://crt.usertrust.com/USERTrustRSAAddTrustCA.crt'], 'pem': [ 'http://www.gandi.net/static/CAs/GandiStandardSSLCA2.pem'], }, }, 'pro': { 'default': { 'der': ['http://crt.gandi.net/GandiProSSLCA2.crt', 'http://crt.usertrust.com/USERTrustRSAAddTrustCA.crt'], 'pem': ['http://www.gandi.net/static/CAs/GandiProSSLCA2.pem', CROSSED_PEM], }, }, }, } class Certificate(GandiModule): urls = URLS """ Module to handle CLI commands. $ gandi certificate change-dcv $ gandi certificate create $ gandi certificate delete $ gandi certificate export $ gandi certificate info $ gandi certificate list $ gandi certificate packages $ gandi certificate resend-dcv $ gandi certificate update """ @classmethod def get_latest_valid(cls, hosts): """ Retrieve valid certificates by fqdn. """ certs = cls.list({'status': 'valid', 'items_per_page': 500}) possible = None if not isinstance(hosts, (tuple, list)): hosts = [hosts] for cert in certs: cert_hosts = set([cert['cn']] + cert['altnames']) if len(set(hosts) - cert_hosts) == 0: if (possible and possible['date_end'] < cert['date_end'] or not possible): possible = cert return possible @classmethod def from_cn(cls, common_name): """ Retrieve a certificate by its common name. """ # search with cn result_cn = [(cert['id'], [cert['cn']] + cert['altnames']) for cert in cls.list({'status': ['pending', 'valid'], 'items_per_page': 500, 'cn': common_name})] # search with altname result_alt = [(cert['id'], [cert['cn']] + cert['altnames']) for cert in cls.list({'status': ['pending', 'valid'], 'items_per_page': 500, 'altname': common_name})] result = result_cn + result_alt ret = {} for id_, fqdns in result: for fqdn in fqdns: ret.setdefault(fqdn, []).append(id_) cert_id = ret.get(common_name) if not cert_id: return return cert_id @classmethod def usable_ids(cls, id, accept_multi=True): """ Retrieve id from input which can be an id or a cn.""" try: qry_id = [int(id)] except ValueError: try: qry_id = cls.from_cn(id) except Exception: qry_id = None if not qry_id or not accept_multi and len(qry_id) != 1: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id if accept_multi else qry_id[0] @classmethod def usable_id(cls, id): """ Retrieve id from single input.""" return cls.usable_ids(id, False) @classmethod def package_list(cls, options=None): """ List possible certificate packages.""" options = options or {} try: return cls.safe_call('cert.package.list', options) except UsageError as err: if err.code == 150020: return [] raise __packages__ = None @classmethod def package_get(cls, package_name): if not cls.__packages__: cls.__packages__ = dict([(pkg['name'], pkg) for pkg in cls.package_list()]) return cls.__packages__.get(package_name) @classmethod def list(cls, options=None): """ List certificates.""" options = options or {} return cls.call('cert.list', options) @classmethod def info(cls, id): """ Display information about a certificate.""" return cls.call('cert.info', id) @classmethod def get_package(cls, common_name, type='std', max_altname=None, altnames=None, warranty=None): type = type or 'std' if max_altname: if max_altname < len(altnames): cls.echo('You choose --max-altname %s but you have more ' 'altnames (%s)' % (max_altname, len(altnames))) return else: if '*' in common_name: max_altname = 'w' elif not altnames: max_altname = 1 else: for max_ in [1, 3, 5, 10, 20]: if len(altnames) < max_: max_altname = max_ break if not max_altname: cls.echo('Too many altnames, max is 20.') return pack_filter = 'cert_%s_%s_' % (type, max_altname) if warranty: pack_filter += '%s_' % (warranty) packages = [item['name'] for item in cls.package_list() if item['name'].startswith(pack_filter)] return packages[0] if packages else None @classmethod def advice_dcv_method(cls, csr, package, altnames, dcv_method): """ Display dcv_method information. """ params = {'csr': csr, 'package': package, 'dcv_method': dcv_method} result = cls.call('cert.get_dcv_params', params) if dcv_method == 'dns': cls.echo('You have to add these records in your domain zone :') cls.echo('\n'.join(result['message'])) @classmethod def change_dcv(cls, oper_id, dcv_method): """ Change dcv method.""" cls.call('cert.change_dcv', oper_id, dcv_method) @classmethod def resend_dcv(cls, oper_id): """ Resend dcv. """ cls.call('cert.resend_dcv', oper_id) @classmethod def create(cls, csr, duration, package, altnames=None, dcv_method=None): """ Create a new certificate. """ params = {'csr': csr, 'package': package, 'duration': duration} if altnames: params['altnames'] = altnames if dcv_method: params['dcv_method'] = dcv_method if dcv_method in ('dns', 'file'): cls.advice_dcv_method(csr, package, altnames, dcv_method) try: result = cls.call('cert.create', params) except UsageError: params['--dry-run'] = True msg = '\n'.join(['%s (%s)' % (err['reason'], err['attr']) for err in cls.call('cert.create', params)]) cls.error(msg) raise return result @classmethod def update(cls, cert_id, csr, private_key, country, state, city, organisation, branch, altnames, dcv_method): """ Update a certificate. """ cert = cls.info(cert_id) if cert['status'] != 'valid': cls.error('The certificate must be in valid status to be updated.') return common_name = cert['cn'] csr = cls.process_csr(common_name, csr, private_key, country, state, city, organisation, branch) if not csr: return params = {'csr': csr} if altnames: params['altnames'] = [] for altname in altnames: params['altnames'].extend(altname.split(',')) if dcv_method: params['dcv_method'] = dcv_method try: result = cls.call('cert.update', cert_id, params) except UsageError: params['--dry-run'] = True msg = cls.call('cert.update', cert_id, params) if msg: cls.error(str(msg)) raise return result @staticmethod def private_key(common_name): return common_name.replace('*.', 'wildcard.') + '.key' @classmethod def gen_pk(cls, common_name, private_key): if private_key: cmd = 'openssl req -new -key %(key)s -out %(csr)s -subj "%(subj)s"' if not os.path.exists(private_key): content = private_key private_key = cls.private_key(common_name) with open(private_key, 'w') as fhandle: fhandle.write(content) else: private_key = cls.private_key(common_name) # TODO check if it exists cmd = ('openssl req -new -newkey rsa:2048 -sha256 -nodes ' '-out %(csr)s -keyout %(key)s -subj "%(subj)s"') return cmd, private_key @classmethod def create_csr(cls, common_name, private_key=None, params=None): """ Create CSR. """ params = params or [] params = [(key, val) for key, val in params if val] subj = '/' + '/'.join(['='.join(value) for value in params]) cmd, private_key = cls.gen_pk(common_name, private_key) if private_key.endswith('.crt') or private_key.endswith('.key'): csr_file = re.sub(r'\.(crt|key)$', '.csr', private_key) else: csr_file = private_key + '.csr' cmd = cmd % {'csr': csr_file, 'key': private_key, 'subj': subj} result = cls.execute(cmd) if not result: cls.echo('CSR creation failed') cls.echo(cmd) return return csr_file @classmethod def get_common_name(cls, csr): """ Read information from CSR. """ from tempfile import NamedTemporaryFile fhandle = NamedTemporaryFile() fhandle.write(csr.encode('latin1')) fhandle.flush() output = cls.exec_output('openssl req -noout -subject -in %s' % fhandle.name) if not output: return common_name = output.split('=')[-1].strip() fhandle.close() return common_name @classmethod def process_csr(cls, common_name, csr=None, private_key=None, country=None, state=None, city=None, organisation=None, branch=None): """ Create a PK and a CSR if needed.""" if csr: if branch or organisation or city or state or country: cls.echo('Following options are only used to generate' ' the CSR.') else: params = (('CN', common_name), ('OU', branch), ('O', organisation), ('L', city), ('ST', state), ('C', country)) params = [(key, val) for key, val in params if val] csr = cls.create_csr(common_name, private_key, params) if csr and os.path.exists(csr): with open(csr) as fcsr: csr = fcsr.read() return csr @classmethod def pretty_format_cert(cls, cert): """ Pretty display of a certificate.""" crt = cert.get('cert') if crt: crt = ('-----BEGIN CERTIFICATE-----\n' + '\n'.join([crt[index * 64:(index + 1) * 64] for index in range(int(len(crt) / 64) + 1)]).rstrip('\n') + # noqa '\n-----END CERTIFICATE-----') return crt @classmethod def delete(cls, cert_id, background=False): """ Delete a certificate.""" result = cls.call('cert.delete', cert_id) if background: return result cls.echo("Deleting your certificate.") cls.display_progress(result) cls.echo('Your certificate %s has been deleted.' % cert_id) return result gandi.cli-1.2/gandi/cli/modules/__init__.py0000644000175000017500000000012212441335654021476 0ustar sayounsayoun00000000000000""" Contains CLI modules used by commands. One module per command namespace. """ gandi.cli-1.2/gandi/cli/modules/paas.py0000644000175000017500000002715713046322151020672 0ustar sayounsayoun00000000000000""" PaaS commands module. """ import re import sys from gandi.cli.core.base import GandiModule from gandi.cli.modules.metric import Metric from gandi.cli.modules.vhost import Vhost from gandi.cli.modules.datacenter import Datacenter from gandi.cli.modules.sshkey import SshkeyHelper class Paas(GandiModule, SshkeyHelper): """ Module to handle CLI commands. $ gandi paas attach $ gandi paas clone $ gandi paas console $ gandi paas create $ gandi paas delete $ gandi paas info $ gandi paas list $ gandi paas restart $ gandi paas types $ gandi paas update """ @classmethod def type_list(cls, options=None): """List type of PaaS instances.""" return cls.safe_call('paas.type.list', options) @classmethod def clone(cls, name, vhost, directory, origin): """Clone a PaaS instance's vhost into a local git repository.""" paas_info = cls.info(name) if 'php' in paas_info['type'] and not vhost: cls.error('PHP instances require indicating the VHOST to clone ' 'with --vhost ') paas_access = '%s@%s' % (paas_info['user'], paas_info['git_server']) remote_url = 'ssh+git://%s/%s.git' % (paas_access, vhost) command = 'git clone %s %s --origin %s' \ % (remote_url, directory, origin) init_git = cls.execute(command) if init_git: cls.echo('Use `git push %s master` to push your code to the ' 'instance.' % (origin)) cls.echo('Then `$ gandi deploy` to build and deploy your ' 'application.') @classmethod def attach(cls, name, vhost, remote_name): """Attach an instance's vhost to a remote from the local repository.""" paas_access = cls.get('paas_access') if not paas_access: paas_info = cls.info(name) paas_access = '%s@%s' \ % (paas_info['user'], paas_info['git_server']) remote_url = 'ssh+git://%s/%s.git' % (paas_access, vhost) ret = cls.execute('git remote add %s %s' % (remote_name, remote_url,)) if ret: cls.echo('Added remote `%s` to your local git repository.' % (remote_name)) cls.echo('Use `git push %s master` to push your code to the ' 'instance.' % (remote_name)) cls.echo('Then `$ gandi deploy` to build and deploy your ' 'application.') @classmethod def deploy(cls, remote_name, branch): """Deploy a PaaS instance.""" def get_remote_url(remote): return 'git config --local --get remote.%s.url' % (remote) remote_url = cls.exec_output(get_remote_url(remote_name)) \ .replace('\n', '') if not remote_url or not re.search('gpaas.net|gandi.net', remote_url): remote_name = ('$(git config --local --get branch.%s.remote)' % branch) remote_url = cls.exec_output(get_remote_url(remote_name)) \ .replace('\n', '') error = None if not remote_url: error = True cls.echo('Error: Could not find git remote ' 'to extract deploy url from.') elif not re.search('gpaas.net|gandi.net', remote_url): error = True cls.echo('Error: %s is not a valid Simple Hosting git remote.' % (remote_url)) if error: cls.echo("""This usually happens when: - the current directory has no Simple Hosting git remote attached, in this case, please see $ gandi paas attach --help - the local branch being deployed hasn't been pushed to the \ remote repository yet, in this case, please try $ git push %s """ % (branch)) cls.echo('Otherwise, it\'s recommended to use' ' the --remote and/or --branch options:\n' '$ gandi deploy --remote [--branch ]') sys.exit(2) remote_url_no_protocol = remote_url.split('://')[1] splitted_url = remote_url_no_protocol.split('/') paas_access = splitted_url[0] deploy_git_host = splitted_url[1] command = "ssh %s 'deploy %s %s'" \ % (paas_access, deploy_git_host, branch) cls.execute(command) @classmethod def list(cls, options=None): """List PaaS instances.""" return cls.call('paas.list', options) @classmethod def info(cls, id): """Display information about a PaaS instance.""" return cls.call('paas.info', cls.usable_id(id)) @classmethod def quota(cls, id): """return disk quota used/free""" sampler = {'unit': 'minutes', 'value': 1, 'function': 'avg'} query = 'vfs.df.bytes.all' metrics = Metric.query(id, 60, query, 'paas', sampler) df = {'free': 0, 'used': 0} for metric in metrics: what = metric['size'].pop() # we need the most recent points metric['points'].reverse() for point in metric['points']: if 'value' in point: df[what] = point['value'] break return df @classmethod def cache(cls, id): """return the number of query cache for the last 24H""" sampler = {'unit': 'days', 'value': 1, 'function': 'sum'} query = 'webacc.requests.cache.all' metrics = Metric.query(id, 60 * 60 * 24, query, 'paas', sampler) cache = {'hit': 0, 'miss': 0, 'not': 0, 'pass': 0} for metric in metrics: what = metric['cache'].pop() for point in metric['points']: value = point.get('value', 0) cache[what] += value return cache @classmethod def delete(cls, resources, background=False): """Delete a PaaS instance.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('paas.delete', cls.usable_id(item)) if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar cls.echo('Deleting your PaaS instance.') cls.display_progress(opers) @classmethod def update(cls, id, name, size, quantity, password, sshkey, upgrade, console, snapshot_profile, reset_mysql_password, background): """Update a PaaS instance.""" if not background and not cls.intty(): background = True paas_params = {} if name: paas_params['name'] = name if size: paas_params['size'] = size if quantity: paas_params['quantity'] = quantity if password: paas_params['password'] = password paas_params.update(cls.convert_sshkey(sshkey)) if upgrade: paas_params['upgrade'] = upgrade if console: paas_params['console'] = console # XXX to delete a snapshot_profile the value has to be an empty string if snapshot_profile is not None: paas_params['snapshot_profile'] = snapshot_profile if reset_mysql_password: paas_params['reset_mysql_password'] = reset_mysql_password result = cls.call('paas.update', cls.usable_id(id), paas_params) if background: return result # interactive mode, run a progress bar cls.echo('Updating your PaaS instance.') cls.display_progress(result) @classmethod def create(cls, name, size, type, quantity, duration, datacenter, vhosts, password, snapshot_profile, background, sshkey): """Create a new PaaS instance.""" if not background and not cls.intty(): background = True datacenter_id_ = int(Datacenter.usable_id(datacenter)) paas_params = { 'name': name, 'size': size, 'type': type, 'duration': duration, 'datacenter_id': datacenter_id_, } if password: paas_params['password'] = password if quantity: paas_params['quantity'] = quantity paas_params.update(cls.convert_sshkey(sshkey)) if snapshot_profile: paas_params['snapshot_profile'] = snapshot_profile result = cls.call('paas.create', paas_params) if not background: # interactive mode, run a progress bar cls.echo('Creating your PaaS instance.') cls.display_progress(result) cls.echo('Your PaaS instance %s has been created.' % name) if vhosts: paas_info = cls.info(name) Vhost.create(paas_info, vhosts, True, background) return result @classmethod def restart(cls, resources, background=False): """Restart a PaaS instance.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('paas.restart', cls.usable_id(item)) if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar cls.echo('Restarting your PaaS instance.') cls.display_progress(opers) @classmethod def resource_list(cls): """ Get the possible list of resources (name, id and vhosts). """ items = cls.list({'items_per_page': 500}) ret = [paas['name'] for paas in items] ret.extend([str(paas['id']) for paas in items]) for paas in items: paas = cls.info(paas['id']) ret.extend([vhost['name'] for vhost in paas['vhosts']]) return ret @classmethod def console(cls, id): """Open a console to a PaaS instance.""" oper = cls.call('paas.update', cls.usable_id(id), {'console': 1}) cls.echo('Activation of the console on your PaaS instance') cls.display_progress(oper) console_url = Paas.info(cls.usable_id(id))['console'] access = 'ssh %s' % console_url cls.execute(access) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be hostname, vhost, id.""" try: # id is maybe a hostname qry_id = cls.from_hostname(id) if not qry_id: # id is maybe a vhost qry_id = cls.from_vhost(id) if not qry_id: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def from_vhost(cls, vhost): """Retrieve paas instance id associated to a vhost.""" result = Vhost().list() paas_hosts = {} for host in result: paas_hosts[host['name']] = host['paas_id'] return paas_hosts.get(vhost) @classmethod def from_hostname(cls, hostname): """Retrieve paas instance id associated to a host.""" result = cls.list({'items_per_page': 500}) paas_hosts = {} for host in result: paas_hosts[host['name']] = host['id'] return paas_hosts.get(hostname) @classmethod def list_names(cls): """Retrieve paas id and names.""" ret = dict([(item['id'], item['name']) for item in cls.list({'items_per_page': 500})]) return ret gandi.cli-1.2/gandi/cli/modules/sshkey.py0000644000175000017500000000562612663546474021275 0ustar sayounsayoun00000000000000""" SSH key commands module. """ import os from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import DuplicateResults class Sshkey(GandiModule): """ Module to handle CLI commands. $ gandi sshkey create $ gandi sshkey delete $ gandi sshkey info $ gandi sshkey list """ @classmethod def from_name(cls, name): """Retrieve a sshkey id associated to a name.""" sshkeys = cls.list({'name': name}) if len(sshkeys) == 1: return sshkeys[0]['id'] elif not sshkeys: return raise DuplicateResults('sshkey name %s is ambiguous.' % name) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be name or id.""" try: # id is maybe a sshkey name qry_id = cls.from_name(id) if not qry_id: qry_id = int(id) except DuplicateResults as exc: cls.error(exc.errors) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def list(cls, options=None): """ List ssh keys.""" options = options if options else {} return cls.call('hosting.ssh.list', options) @classmethod def info(cls, id): """ Display information about an ssh key. """ return cls.call('hosting.ssh.info', cls.usable_id(id)) @classmethod def create(cls, name, value): """ Create a new ssh key.""" sshkey_params = { 'name': name, 'value': value, } result = cls.call('hosting.ssh.create', sshkey_params) return result @classmethod def delete(cls, id): """Delete this ssh key.""" return cls.call('hosting.ssh.delete', cls.usable_id(id)) class SshkeyHelper(object): """ Helper class to handle sshkey configuration entry. """ @classmethod def convert_sshkey(cls, sshkey): """ Return dict param with valid entries for vm/paas methods. """ params = {} if sshkey: params['keys'] = [] for ssh in sshkey: if os.path.exists(os.path.expanduser(ssh)): if 'ssh_key' in params: cls.echo("Can't have more than one sshkey file.") continue with open(ssh) as fdesc: sshkey_ = fdesc.read() if sshkey_: params['ssh_key'] = sshkey_ else: sshkey_id = Sshkey.usable_id(ssh) if sshkey_id: params['keys'].append(sshkey_id) else: cls.echo('This is not a ssh key %s' % ssh) if not params['keys']: params.pop('keys') return params gandi.cli-1.2/gandi/cli/modules/metric.py0000644000175000017500000000171012506522740021222 0ustar sayounsayoun00000000000000""" metric module. """ import time from datetime import datetime from gandi.cli.core.base import GandiModule class Metric(GandiModule): """ Module to query metrics """ @classmethod def query(cls, resources, time_range, query, resource_type, sampler): """Query statistics for given resources.""" if not isinstance(resources, (list, tuple)): resources = [resources] now = time.time() start_utc = datetime.utcfromtimestamp(now - time_range) end_utc = datetime.utcfromtimestamp(now) date_format = '%Y-%m-%d %H:%M:%S' start = start_utc.strftime(date_format) end = end_utc.strftime(date_format) query = {'start': start, 'end': end, 'query': query, 'resource_id': resources, 'resource_type': resource_type, 'sampler': sampler} return cls.call('hosting.metric.query', query) gandi.cli-1.2/gandi/cli/modules/vhost.py0000644000175000017500000000321212653625522021106 0ustar sayounsayoun00000000000000""" Vhost commands module. """ import os from gandi.cli.core.base import GandiModule class Vhost(GandiModule): """ Module to handle CLI commands. $ gandi vhost create $ gandi vhost delete $ gandi vhost info $ gandi vhost list """ @classmethod def list(cls, options=None): """ List paas vhosts (in the future it should handle iaas vhosts).""" options = options or {} return cls.call('paas.vhost.list', options) @classmethod def info(cls, name): """ Display information about a vhost. """ return cls.call('paas.vhost.info', name) @classmethod def create(cls, paas_info, vhost, alter_zone, background): """ Create a new vhost. """ if not background and not cls.intty(): background = True params = {'paas_id': paas_info['id'], 'vhost': vhost, 'zone_alter': alter_zone} result = cls.call('paas.vhost.create', params, dry_run=True) if background: return result cls.echo('Creating a new vhost.') cls.display_progress(result) cls.echo('Your vhost %s has been created.' % vhost) return result @classmethod def delete(cls, resources, background=False): """ Delete this vhost. """ if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('paas.vhost.delete', item) opers.append(oper) if background: return opers cls.echo('Deleting your vhost.') cls.display_progress(opers) gandi.cli-1.2/gandi/cli/modules/iaas.py0000644000175000017500000005037413227414346020671 0ustar sayounsayoun00000000000000""" VM commands module. """ import math import os import socket import time import errno from gandi.cli.core.base import GandiModule from gandi.cli.core.utils import randomstring from gandi.cli.modules.datacenter import Datacenter from gandi.cli.modules.sshkey import SshkeyHelper from gandi.cli.core.utils import MigrationNotFinalized class Iaas(GandiModule, SshkeyHelper): """ Module to handle CLI commands. $ gandi vm console $ gandi vm create $ gandi vm delete $ gandi vm images $ gandi vm info $ gandi vm kernels $ gandi vm list $ gandi vm reboot $ gandi vm ssh $ gandi vm start $ gandi vm stop $ gandi vm update """ @classmethod def list(cls, options=None): """List virtual machines.""" if not options: options = {} return cls.call('hosting.vm.list', options) @classmethod def resource_list(cls): """ Get the possible list of resources (hostname, id). """ items = cls.list({'items_per_page': 500}) ret = [vm['hostname'] for vm in items] ret.extend([str(vm['id']) for vm in items]) return ret @classmethod def info(cls, id): """Display information about a virtual machine.""" return cls.call('hosting.vm.info', cls.usable_id(id)) @classmethod def stop(cls, resources, background=False): """Stop a virtual machine.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('hosting.vm.stop', cls.usable_id(item)) if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar instance_info = "'%s'" % ', '.join(resources) cls.echo('Stopping your Virtual Machine(s) %s.' % instance_info) cls.display_progress(opers) @classmethod def start(cls, resources, background=False): """Start a virtual machine.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('hosting.vm.start', cls.usable_id(item)) if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar instance_info = "'%s'" % ', '.join(resources) cls.echo('Starting your Virtual Machine(s) %s.' % instance_info) cls.display_progress(opers) @classmethod def reboot(cls, resources, background=False): """Reboot a virtual machine.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('hosting.vm.reboot', cls.usable_id(item)) if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar instance_info = "'%s'" % ', '.join(resources) cls.echo('Rebooting your Virtual Machine(s) %s.' % instance_info) cls.display_progress(opers) @classmethod def delete(cls, resources, background=False): """Delete a virtual machine.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('hosting.vm.delete', cls.usable_id(item)) if not oper: continue if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar instance_info = "'%s'" % ', '.join(resources) cls.echo('Deleting your Virtual Machine(s) %s.' % instance_info) if opers: cls.display_progress(opers) @classmethod def required_max_memory(cls, id, memory): """ Recommend a max_memory setting for this vm given memory. If the VM already has a nice setting, return None. The max_memory param cannot be fixed too high, because page table allocation would cost too much for small memory profile. Use a range as below. """ best = int(max(2 ** math.ceil(math.log(memory, 2)), 2048)) actual_vm = cls.info(id) if (actual_vm['state'] == 'running' and actual_vm['vm_max_memory'] != best): return best @classmethod def update(cls, id, memory, cores, console, password, background, max_memory): """Update a virtual machine.""" if not background and not cls.intty(): background = True vm_params = {} if memory: vm_params['memory'] = memory if cores: vm_params['cores'] = cores if console: vm_params['console'] = console if password: vm_params['password'] = password if max_memory: vm_params['vm_max_memory'] = max_memory result = cls.call('hosting.vm.update', cls.usable_id(id), vm_params) if background: return result # interactive mode, run a progress bar cls.echo('Updating your Virtual Machine %s.' % id) cls.display_progress(result) @classmethod def create(cls, datacenter, memory, cores, ip_version, bandwidth, login, password, hostname, image, run, background, sshkey, size, vlan, ip, script, script_args, ssh): """Create a new virtual machine.""" from gandi.cli.modules.network import Ip, Iface if not background and not cls.intty(): background = True datacenter_id_ = int(Datacenter.usable_id(datacenter)) if not hostname: hostname = randomstring('vm') disk_name = 'sys_%s' % hostname[2:] else: disk_name = 'sys_%s' % hostname.replace('.', '') vm_params = { 'hostname': hostname, 'datacenter_id': datacenter_id_, 'memory': memory, 'cores': cores, } if login: vm_params['login'] = login if run: vm_params['run'] = run if password: vm_params['password'] = password if ip_version: vm_params['ip_version'] = ip_version vm_params['bandwidth'] = bandwidth if script: with open(script) as fd: vm_params['script'] = fd.read() if script_args: vm_params['script_args'] = script_args vm_params.update(cls.convert_sshkey(sshkey)) # XXX: name of disk is limited to 15 chars in ext2fs, ext3fs # but api allow 255, so we limit to 15 for now disk_params = {'datacenter_id': vm_params['datacenter_id'], 'name': disk_name[:15]} if size: if isinstance(size, tuple): prefix, size = size disk_params['size'] = size sys_disk_id_ = int(Image.usable_id(image, datacenter_id_)) ip_summary = [] if ip_version == 4: ip_summary = ['v4', 'v6'] elif ip_version == 6: ip_summary = ['v6'] if vlan: ip_ = None ip_summary.append('private') if ip: try: ip_ = Ip.info(ip) except Exception: pass else: if not Ip._check_and_detach(ip_, None): return if ip_: iface_id = ip_['iface_id'] else: ip_create = Ip.create(4, vm_params['datacenter_id'], bandwidth, None, vlan, ip) iface_id = ip_create['iface_id'] # if there is a public ip, will attach this one later, else give # the iface to vm.create if not ip_version: vm_params['iface_id'] = iface_id result = cls.call('hosting.vm.create_from', vm_params, disk_params, sys_disk_id_) cls.echo('* Configuration used: %d cores, %dMb memory, ip %s, ' 'image %s, hostname: %s, datacenter: %s' % (cores, memory, '+'.join(ip_summary), image, hostname, datacenter)) # background mode, bail out now (skip interactive part) if background and (not vlan or not ip_version): return result # interactive mode, run a progress bar cls.echo('Creating your Virtual Machine %s.' % hostname) cls.display_progress(result) cls.echo('Your Virtual Machine %s has been created.' % hostname) vm_id = None for oper in result: if oper.get('vm_id'): vm_id = oper.get('vm_id') break if vlan and ip_version: attach = Iface._attach(iface_id, vm_id) if background: return attach if 'ssh_key' not in vm_params and 'keys' not in vm_params: return if vm_id and ip_version: cls.wait_for_sshd(vm_id) if ssh: cls.ssh_keyscan(vm_id) cls.ssh(vm_id, 'root', None) @classmethod def need_finalize(cls, resource): """Check if vm migration need to be finalized.""" vm_id = cls.usable_id(resource) params = {'type': 'hosting_migration_vm', 'step': 'RUN', 'vm_id': vm_id} result = cls.call('operation.list', params) if not result or len(result) > 1: raise MigrationNotFinalized('Cannot find VM %s ' 'migration operation.' % resource) need_finalize = result[0]['params']['inner_step'] == 'wait_finalize' if not need_finalize: raise MigrationNotFinalized('VM %s migration does not need ' 'finalization.' % resource) @classmethod def check_can_migrate(cls, resource): """Check if virtual machine can be migrated to another datacenter.""" vm_id = cls.usable_id(resource) result = cls.call('hosting.vm.can_migrate', vm_id) if not result['can_migrate']: if result['matched']: matched = result['matched'][0] cls.echo('Your VM %s cannot be migrated yet. Migration will ' 'be available when datacenter %s is opened.' % (resource, matched)) else: cls.echo('Your VM %s cannot be migrated.' % resource) return False return True @classmethod def migrate(cls, resource, background=False, finalize=False): """ Migrate a virtual machine to another datacenter. """ vm_id = cls.usable_id(resource) if finalize: verb = 'Finalizing' result = cls.call('hosting.vm.migrate', vm_id, True) else: verb = 'Starting' result = cls.call('hosting.vm.migrate', vm_id, False) dcs = {} for dc in Datacenter.list(): dcs[dc['id']] = dc['dc_code'] oper = cls.call('operation.info', result['id']) dc_from = dcs[oper['params']['from_dc_id']] dc_to = dcs[oper['params']['to_dc_id']] migration_msg = ('* %s the migration of VM %s ' 'from datacenter %s to %s' % (verb, resource, dc_from, dc_to)) cls.echo(migration_msg) if background: return result cls.echo('VM migration in progress.') cls.display_progress(result) cls.echo('Your VM %s has been migrated.' % resource) return result @classmethod def from_hostname(cls, hostname): """Retrieve virtual machine id associated to a hostname.""" result = cls.list({'hostname': str(hostname)}) if result: return result[0]['id'] @classmethod def usable_id(cls, id): """ Retrieve id from input which can be hostname or id.""" try: # id is maybe a hostname qry_id = cls.from_hostname(id) if not qry_id: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def vm_ip(cls, vm_id): """Return the first usable ip address for this vm. Returns a (version, ip) tuple.""" vm_info = cls.info(vm_id) for iface in vm_info['ifaces']: if iface['type'] == 'private': continue for ip in iface['ips']: return ip['version'], ip['ip'] @classmethod def wait_for_sshd(cls, vm_id): """Insist on having the vm booted and sshd listening""" cls.echo('Waiting for the vm to come online') version, ip_addr = cls.vm_ip(vm_id) give_up = time.time() + 300 last_error = None while time.time() < give_up: try: inet = socket.AF_INET if version == 6: inet = socket.AF_INET6 sd = socket.socket(inet, socket.SOCK_STREAM, socket.IPPROTO_TCP) sd.settimeout(5) sd.connect((ip_addr, 22)) sd.recv(1024) return except socket.error as err: if err.errno == errno.EHOSTUNREACH and version == 6: cls.error('%s is not reachable, you may be missing ' 'IPv6 connectivity' % ip_addr) last_error = err time.sleep(1) except Exception as err: last_error = err time.sleep(1) cls.error('VM did not spin up (last error: %s)' % last_error) @classmethod def ssh_keyscan(cls, vm_id): """Wipe this old key and learn the new one from a freshly created vm. This is a security risk for this VM, however we dont have another way to learn the key yet, so do this for the user.""" cls.echo('Wiping old key and learning the new one') _version, ip_addr = cls.vm_ip(vm_id) cls.execute('ssh-keygen -R "%s"' % ip_addr) for _ in range(5): output = cls.exec_output('ssh-keyscan "%s"' % ip_addr) if output: with open(os.path.expanduser('~/.ssh/known_hosts'), 'a') as f: f.write(output) return True time.sleep(.5) @classmethod def scp(cls, vm_id, login, identity, local_file, remote_file): """Copy file to remote VM.""" cmd = ['scp'] if identity: cmd.extend(('-i', identity,)) version, ip_addr = cls.vm_ip(vm_id) if version == 6: ip_addr = '[%s]' % ip_addr cmd.extend((local_file, '%s@%s:%s' % (login, ip_addr, remote_file),)) cls.echo('Running %s' % ' '.join(cmd)) for _ in range(5): ret = cls.execute(cmd, False) if ret: break time.sleep(.5) return ret @classmethod def ssh(cls, vm_id, login, identity, args=None): """Spawn an ssh session to virtual machine.""" cmd = ['ssh'] if identity: cmd.extend(('-i', identity,)) version, ip_addr = cls.vm_ip(vm_id) if version == 6: cmd.append('-6') if not ip_addr: cls.echo('No IP address found for vm %s, aborting.' % vm_id) return cmd.append('%s@%s' % (login, ip_addr,)) if args: cmd.extend(args) cls.echo('Requesting access using: %s ...' % ' '.join(cmd)) return cls.execute(cmd, False) @classmethod def console(cls, id): """Open a console to virtual machine.""" vm_info = cls.info(id) if not vm_info['console']: # first activate console cls.update(id, memory=None, cores=None, console=True, password=None, background=False, max_memory=None) # now we can connect # retrieve ip of vm vm_info = cls.info(id) version, ip_addr = cls.vm_ip(id) console_url = vm_info.get('console_url', 'console.gandi.net') access = 'ssh %s@%s' % (ip_addr, console_url) cls.execute(access) class Image(GandiModule): """ Module to handle CLI commands. $ gandi vm images """ @classmethod def list(cls, datacenter=None, label=None): """List available images for vm creation.""" options = {} if datacenter: datacenter_id = int(Datacenter.usable_id(datacenter)) options['datacenter_id'] = datacenter_id # implement a filter by label as API doesn't handle it images = cls.safe_call('hosting.image.list', options) if not label: return images return [img for img in images if label.lower() in img['label'].lower()] @classmethod def is_deprecated(cls, label, datacenter=None): """Check if image if flagged as deprecated.""" images = cls.list(datacenter, label) images_visibility = dict([(image['label'], image['visibility']) for image in images]) return images_visibility.get(label, 'all') == 'deprecated' @classmethod def from_label(cls, label, datacenter=None): """Retrieve disk image id associated to a label.""" result = cls.list(datacenter=datacenter) image_labels = dict([(image['label'], image['disk_id']) for image in result]) return image_labels.get(label) @classmethod def from_sysdisk(cls, label): """Retrieve disk id from available system disks""" disks = cls.safe_call('hosting.disk.list', {'name': label}) if len(disks): return disks[0]['id'] @classmethod def usable_id(cls, id, datacenter=None): """ Retrieve id from input which can be label or id.""" try: qry_id = int(id) except Exception: # if id is a string, prefer a system disk then a label qry_id = cls.from_sysdisk(id) or cls.from_label(id, datacenter) if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id class Kernel(GandiModule): """ Module to handle Gandi Kernels. """ @classmethod def list(cls, datacenter=None, flavor=None, match='', exact_match=False): """ List available kernels for datacenter.""" if not datacenter: dc_ids = [dc['id'] for dc in Datacenter.filtered_list()] kmap = {} for dc_id in dc_ids: vals = cls.safe_call('hosting.disk.list_kernels', dc_id) for key in vals: kmap.setdefault(key, []).extend(vals.get(key, [])) # remove duplicates for key in kmap: kmap[key] = list(set(kmap[key])) else: dc_id = Datacenter.usable_id(datacenter) kmap = cls.safe_call('hosting.disk.list_kernels', dc_id) if match: for flav in kmap: if exact_match: kmap[flav] = [x for x in kmap[flav] if match == x] else: kmap[flav] = [x for x in kmap[flav] if match in x] if flavor: if flavor not in kmap: cls.error('flavor %s not supported here' % flavor) return dict([(flavor, kmap[flavor])]) return kmap @classmethod def is_available(cls, disk, kernel): """ Check if kernel is available for disk.""" kmap = cls.list(disk['datacenter_id'], None, kernel, True) for flavor in kmap: if kernel in kmap[flavor]: return True return False gandi.cli-1.2/gandi/cli/modules/mail.py0000644000175000017500000000501012656121545020662 0ustar sayounsayoun00000000000000""" Mail commands module. """ from gandi.cli.core.base import GandiModule class Mail(GandiModule): """ Module to handle CLI commands. $ gandi mail create $ gandi mail delete $ gandi mail info $ gandi mail list $ gandi mail purge $ gandi mail update """ @classmethod def list(cls, domain, options): """List mailboxes for a given domain name.""" return cls.call('domain.mailbox.list', domain, options) @classmethod def info(cls, domain, login): """Display information about a mailbox.""" return cls.call('domain.mailbox.info', domain, login) @classmethod def create(cls, domain, login, options, alias): """Create a mailbox.""" cls.echo('Creating your mailbox.') result = cls.call('domain.mailbox.create', domain, login, options) if alias: cls.echo('Creating aliases.') result = cls.set_alias(domain, login, list(alias)) return result @classmethod def delete(cls, domain, login): """Delete a mailbox.""" return cls.call('domain.mailbox.delete', domain, login) @classmethod def update(cls, domain, login, options, alias_add, alias_del): """Update a mailbox.""" cls.echo('Updating your mailbox.') result = cls.call('domain.mailbox.update', domain, login, options) if alias_add or alias_del: current_aliases = cls.info(domain, login)['aliases'] aliases = current_aliases[:] if alias_add: for alias in alias_add: if alias not in aliases: aliases.append(alias) if alias_del: for alias in alias_del: if alias in aliases: aliases.remove(alias) if ((len(current_aliases) != len(aliases)) or (current_aliases != aliases)): cls.echo('Updating aliases.') result = cls.set_alias(domain, login, aliases) return result @classmethod def purge(cls, domain, login, background=False): """Purge a mailbox.""" oper = cls.call('domain.mailbox.purge', domain, login) if background: return oper else: cls.echo('Purging in progress') cls.display_progress(oper) @classmethod def set_alias(cls, domain, login, aliases): """Update aliases on a mailbox.""" return cls.call('domain.mailbox.alias.set', domain, login, aliases) gandi.cli-1.2/gandi/cli/modules/webacc.py0000644000175000017500000002651012663631621021173 0ustar sayounsayoun00000000000000""" Webaccelerator commands module """ from gandi.cli.core.base import GandiModule from gandi.cli.modules.datacenter import Datacenter class Webacc(GandiModule): """ Module to handle CLI commands. $ gandi webacc list $ gandi webacc info $ gandi webacc create $ gandi webacc add $ gandi webacc delete $ gandi webacc enable $ gandi webacc disable $ gandi webacc probe """ @classmethod def list(cls, options=None): """ List all webaccelerator """ if not options: options = {} return cls.call('hosting.rproxy.list', options) @classmethod def info(cls, id): """ Get information about a Webaccelerator """ return cls.call('hosting.rproxy.info', cls.usable_id(id)) @classmethod def create(cls, name, datacenter, backends, vhosts, algorithm, ssl_enable, zone_alter): """ Create a webaccelerator """ datacenter_id_ = int(Datacenter.usable_id(datacenter)) params = { 'datacenter_id': datacenter_id_, 'name': name, 'lb': {'algorithm': algorithm}, 'override': True, 'ssl_enable': ssl_enable, 'zone_alter': zone_alter } if vhosts: params['vhosts'] = vhosts if backends: params['servers'] = backends try: result = cls.call('hosting.rproxy.create', params) cls.echo('Creating your webaccelerator %s' % params['name']) cls.display_progress(result) cls.echo('Your webaccelerator have been created') return result except Exception as err: if err.code == 580142: for vhost in params['vhosts']: dns_entry = cls.call( 'hosting.rproxy.vhost.get_dns_entries', {'datacenter': datacenter_id_, 'vhost': vhost}) txt_record = "@ 3600 IN TXT \"%s=%s\"" % (dns_entry['key'], dns_entry['txt']) cname_record = "%s 3600 IN CNAME %s" % (dns_entry['key'], dns_entry['cname']) cls.echo('The domain %s don\'t use Gandi DNS or you have' ' not sufficient right to alter the zone file. ' 'Edit your zone file adding this TXT and CNAME ' 'record and try again :' % vhost) cls.echo(txt_record) cls.echo(cname_record) cls.echo('\nOr add a file containing %s at :\n' 'http://%s/%s.txt\n' % (dns_entry['txt'], dns_entry['domain'], dns_entry['txt'])) cls.separator_line('-', 4) else: cls.echo(err) @classmethod def update(cls, resource, new_name, algorithm, ssl_enable, ssl_disable): """ Update a webaccelerator""" params = {} if new_name: params['name'] = new_name if algorithm: params['lb'] = {'algorithm': algorithm} if ssl_enable: params['ssl_enable'] = ssl_enable if ssl_disable: params['ssl_enable'] = False result = cls.call('hosting.rproxy.update', cls.usable_id(resource), params) cls.echo('Updating your webaccelerator') cls.display_progress(result) cls.echo('The webaccelerator have been udated') return result @classmethod def delete(cls, name): """ Delete a webaccelerator """ result = cls.call('hosting.rproxy.delete', cls.usable_id(name)) cls.echo('Deleting your webaccelerator named %s' % name) cls.display_progress(result) cls.echo('Webaccelerator have been deleted') return result @classmethod def backend_list(cls, options): """ List all servers used by webaccelerator """ return cls.call('hosting.rproxy.server.list', options) @classmethod def backend_add(cls, name, backend): """ Add a backend into a webaccelerator """ oper = cls.call( 'hosting.rproxy.server.create', cls.usable_id(name), backend) cls.echo('Adding backend %s:%s into webaccelerator' % (backend['ip'], backend['port'])) cls.display_progress(oper) cls.echo('Backend added') return oper @classmethod def backend_remove(cls, backend): """ Remove a backend on a webaccelerator """ server = cls.backend_list(backend) if server: oper = cls.call('hosting.rproxy.server.delete', server[0]['id']) cls.echo('Removing backend %s:%s into webaccelerator' % (backend['ip'], backend['port'])) cls.display_progress(oper) cls.echo('Your backend have been removed') return oper else: return cls.echo('No backend found') @classmethod def backend_enable(cls, backend): """ Enable a backend for a server """ server = cls.backend_list(backend) if server: oper = cls.call('hosting.rproxy.server.enable', server[0]['id']) cls.echo('Activating backend %s' % server[0]['ip']) cls.display_progress(oper) cls.echo('Backend activated') return oper else: return cls.echo('No backend found') @classmethod def backend_disable(cls, backend): """ Disable a backend for a server """ server = cls.backend_list(backend) oper = cls.call('hosting.rproxy.server.disable', server[0]['id']) cls.echo('Desactivating backend on server %s' % server[0]['ip']) cls.display_progress(oper) cls.echo('Backend desactivated') return oper @classmethod def vhost_list(cls): """ List all vhosts used by webaccelerator """ return cls.call('hosting.rproxy.vhost.list') @classmethod def vhost_add(cls, resource, params): """ Add a vhost into a webaccelerator """ try: oper = cls.call( 'hosting.rproxy.vhost.create', cls.usable_id(resource), params) cls.echo('Adding your virtual host (%s) into %s' % (params['vhost'], resource)) cls.display_progress(oper) cls.echo('Your virtual host habe been added') return oper except Exception as err: if err.code == 580142: dc = cls.info(resource) dns_entry = cls.call('hosting.rproxy.vhost.get_dns_entries', {'datacenter': dc['datacenter']['id'], 'vhost': params['vhost']}) txt_record = "%s 3600 IN TXT \"%s=%s\"" % (dns_entry['key'], dns_entry['key'], dns_entry['txt']) cname_record = "%s 3600 IN CNAME %s" % (dns_entry['key'], dns_entry['cname']) cls.echo('The domain don\'t use Gandi DNS or you have not' ' sufficient right to alter the zone file. ' 'Edit your zone file adding this TXT and CNAME ' 'record and try again :') cls.echo(txt_record) cls.echo(cname_record) cls.echo('\nOr add a file containing %s at :\n' 'http://%s/%s.txt\n' % (dns_entry['txt'], dns_entry['domain'], dns_entry['txt'])) else: cls.echo(err) @classmethod def vhost_remove(cls, name): """ Delete a vhost in a webaccelerator """ oper = cls.call('hosting.rproxy.vhost.delete', name) cls.echo('Deleting your virtual host %s' % name) cls.display_progress(oper) cls.echo('Your virtual host have been removed') return oper @classmethod def probe(cls, resource, enable, disable, test, host, interval, http_method, http_response, threshold, timeout, url, window): """ Set a probe for a webaccelerator """ params = { 'host': host, 'interval': interval, 'method': http_method, 'response': http_response, 'threshold': threshold, 'timeout': timeout, 'url': url, 'window': window } if enable: params['enable'] = True elif disable: params['enable'] = False if test: result = cls.call( 'hosting.rproxy.probe.test', cls.usable_id(resource), params) else: result = cls.call( 'hosting.rproxy.probe.update', cls.usable_id(resource), params) cls.display_progress(result) return result @classmethod def probe_enable(cls, resource): """ Activate a probe on a webaccelerator """ oper = cls.call('hosting.rproxy.probe.enable', cls.usable_id(resource)) cls.echo('Activating probe on %s' % resource) cls.display_progress(oper) cls.echo('The probe have been activated') return oper @classmethod def probe_disable(cls, resource): """ Disable a probe on a webaccelerator """ oper = cls.call('hosting.rproxy.probe.disable', cls.usable_id(resource)) cls.echo('Desactivating probe on %s' % resource) cls.display_progress(oper) cls.echo('The probe have been desactivated') return oper @classmethod def usable_id(cls, id): """ Retrieve id from input which can be hostname, vhost, id. """ try: # id is maybe a hostname qry_id = cls.from_name(id) if not qry_id: # id is maybe an ip qry_id = cls.from_ip(id) if not qry_id: qry_id = cls.from_vhost(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def from_name(cls, name): """Retrieve webacc id associated to a webacc name.""" result = cls.list({'items_per_page': 500}) webaccs = {} for webacc in result: webaccs[webacc['name']] = webacc['id'] return webaccs.get(name) @classmethod def from_ip(cls, ip): """Retrieve webacc id associated to a webacc ip""" result = cls.list({'items_per_page': 500}) webaccs = {} for webacc in result: for server in webacc['servers']: webaccs[server['ip']] = webacc['id'] return webaccs.get(ip) @classmethod def from_vhost(cls, vhost): """Retrieve webbacc id associated to a webacc vhost""" result = cls.list({'items_per_page': 500}) webaccs = {} for webacc in result: for vhost in webacc['vhosts']: webaccs[vhost['name']] = webacc['id'] return webaccs.get(vhost) gandi.cli-1.2/gandi/cli/modules/account.py0000644000175000017500000000233112656121545021377 0ustar sayounsayoun00000000000000""" Hosting account module. """ from gandi.cli.core.base import GandiModule class Account(GandiModule): """ Module to handle CLI commands. $ gandi account info """ @classmethod def info(cls): """Get information about the hosting account in use""" return cls.call('hosting.account.info') @classmethod def creditusage(cls): """Get credit usage per hour""" rating = cls.call('hosting.rating.list') if not rating: return 0 rating = rating.pop() usage = [sum(resource.values()) for resource in rating.values() if isinstance(resource, dict)] return sum(usage) @classmethod def all(cls): """ Get all informations about this account """ account = cls.info() creditusage = cls.creditusage() if not creditusage: return account left = account['credits'] / creditusage years, hours = divmod(left, 365 * 24) months, hours = divmod(hours, 31 * 24) days, hours = divmod(hours, 24) account.update({'credit_usage': creditusage, 'left': (years, months, days, hours)}) return account gandi.cli-1.2/gandi/cli/modules/hostedcert.py0000644000175000017500000001242312663631621022111 0ustar sayounsayoun00000000000000""" Hosted certificate commands module. """ import os from gandi.cli.core.base import GandiModule class HostedCert(GandiModule): """ Module to handle CLI commands. $ gandi certstore list $ gandi certstore info $ gandi certstore create $ gandi certstore delete """ @classmethod def from_fqdn(cls, fqdn): return cls.list({'fqdns': '%s' % fqdn, 'state': 'created'}) @classmethod def usable_id(cls, id): """ Retrieve id from single input. """ hcs = cls.from_fqdn(id) if hcs: return [hc_['id'] for hc_ in hcs] try: return int(id) except (TypeError, ValueError): pass @classmethod def list(cls, options=None): """ List hosted certificates. """ options = options or {} return cls.call('cert.hosted.list', options) @classmethod def info(cls, id): """ Display information about a hosted certificate. """ return cls.call('cert.hosted.info', id) @classmethod def infos(cls, fqdn): """ Display information about hosted certificates for a fqdn. """ if isinstance(fqdn, (list, tuple)): ids = [] for fqd_ in fqdn: ids.extend(cls.infos(fqd_)) return ids ids = cls.usable_id(fqdn) if not ids: return [] if not isinstance(ids, (list, tuple)): ids = [ids] return [cls.info(id_) for id_ in ids] @classmethod def create(cls, key, crt): """ Add a new crt in the hosted cert store. """ options = {'crt': crt, 'key': key} return cls.call('cert.hosted.create', options) @classmethod def delete(cls, id_): """ Remove a cert from the hosted cert store. """ return cls.call('cert.hosted.delete', id_) @classmethod def activate_ssl(cls, vhosts, ssl, private_key, poll_cert): from .cert import Certificate if not ssl: return True if not isinstance(vhosts, (list, tuple)): vhosts = [vhosts] missing = [] for vhost in vhosts: try: hostedcert = cls.infos(vhost) if hostedcert: cls.debug('There is a cert in store for %s' % vhost) else: missing.append(vhost) except ValueError: missing.append(vhost) if not missing: return True vhosts = missing cls.debug('Trying to get certificate or generate it for %s' % (', '.join(vhosts))) vhost = vhosts[0] altnames = vhosts[1:] cert = Certificate.get_latest_valid(vhosts) if cert: if not private_key: cls.echo('Please give the private key for certificate id %s ' '(CN: %s)' % (cert['id'], cert['cn'])) return False cls.echo('Will use the certificate id %s (CN: %s)' % (cert['id'], cert['cn'])) if os.path.isfile(private_key): with open(private_key) as fhandle: private_key = fhandle.read() crt = Certificate.pretty_format_cert(cert) cls.create(private_key, crt) elif poll_cert: cls.echo('This operation will take a long time waiting for the ' 'certificate to be generated.') # create the certificate csr = Certificate.process_csr(vhost, private_key=private_key) package = Certificate.get_package(vhost, altnames=altnames) oper = Certificate.create(csr, 1, package, altnames=altnames) cls.echo('If the term close, you can check the certificate create ' 'operation with :') cls.echo('$ gandi certificate follow %s' % oper['id']) cls.echo("And when it's DONE you can continue doing :") cls.echo('$ gandi certificate export %s' % vhost) cls.echo('$ gandi certstore create --private-key %s --certificate ' '%s' % (vhost.replace('*.', 'wildcard.') + '.key', vhost.replace('*.', 'wildcard.') + '.crt')) cls.echo('And then relaunch the current command.\n') cls.echo('Creating the certificate for %s%s' % (vhost, ' (%s)' % ', '.join(altnames) if altnames else '')) cls.display_progress(oper) # create the hosted certificate. # this will always give us a file name. _, private_key = Certificate.gen_pk(vhost, private_key) with open(private_key) as fhandle: private_key = fhandle.read() cert = Certificate.get_latest_valid(vhosts) crt = Certificate.pretty_format_cert(cert) cls.create(private_key, crt) else: cls.echo('There is no certificate for %s.' % ', '.join(vhosts)) cls.echo('Create the certificate with (for exemple) :') cls.echo('$ gandi certificate create --cn %s --type std %s' % (vhost, '--altnames=%s' % ','.join(altnames) if altnames else '')) cls.echo('Or relaunch the current command with --poll-cert option') return True gandi.cli-1.2/gandi/cli/modules/network.py0000644000175000017500000003117512656121545021444 0ustar sayounsayoun00000000000000""" Iface and vlan commands module. """ from click.exceptions import UsageError from gandi.cli.core.base import GandiModule from gandi.cli.modules.datacenter import Datacenter from gandi.cli.modules.iaas import Iaas class Ip(GandiModule): """ Module to handle CLI commands. $ gandi ip list $ gandi ip info $ gandi ip create $ gandi ip attach $ gandi ip detach $ gandi ip delete """ @classmethod def list(cls, options=None): """List ip""" options = options or {} return cls.call('hosting.ip.list', options) @classmethod def _info(cls, ip_id): """ Get information about an ip.""" return cls.call('hosting.ip.info', ip_id) @classmethod def info(cls, resource): """ Get information about an up.""" return cls._info(cls.usable_id(resource)) @classmethod def create(cls, ip_version, datacenter, bandwidth, vm=None, vlan=None, ip=None, background=False): """ Create a public ip and attach it if vm is given. """ return Iface.create(ip_version, datacenter, bandwidth, vlan, vm, ip, background) @classmethod def update(cls, resource, params, background=False): """ Update this IP """ cls.echo('Updating your IP') result = cls.call('hosting.ip.update', cls.usable_id(resource), params) if not background: cls.display_progress(result) return result @classmethod def resource_list(cls): """ Get the possible list of resources (name, id). """ items = cls.list({'items_per_page': 500}) ret = [str(ip['id']) for ip in items] ret.extend([ip['ip'] for ip in items]) return ret @classmethod def _check_and_detach(cls, ip_, vm_=None): # if the ip exists and is attached, we have to detach it iface = Iface.info(ip_['iface_id']) if iface.get('vm_id'): if vm_ and iface['vm_id'] == vm_.get('id'): return False detach = Iface._detach(iface['id']) cls.display_progress(detach) return True @classmethod def attach(cls, ip, vm, background=False, force=False): """ Attach """ vm_ = Iaas.info(vm) ip_ = cls.info(ip) if not cls._check_and_detach(ip_, vm_): return # then we should attach the ip to the vm attach = Iface._attach(ip_['iface_id'], vm_['id']) if not background: cls.display_progress(attach) return attach @classmethod def _detach(cls, ip_, iface, background=False, force=False): detach = Iface._detach(iface['id']) if background: return detach if detach: cls.display_progress(detach) return detach @classmethod def detach(cls, resource, background=False, force=False): try: ip_ = cls.info(resource) except UsageError: cls.error("Can't find this ip %s" % resource) iface = Iface.info(ip_['iface_id']) return cls._detach(ip_, iface, background, force) @classmethod def delete(cls, resources, background=False, force=False): """Delete an ip by deleting the iface""" if not isinstance(resources, (list, tuple)): resources = [resources] ifaces = [] for item in resources: try: ip_ = cls.info(item) except UsageError: cls.error("Can't find this ip %s" % item) iface = Iface.info(ip_['iface_id']) ifaces.append(iface['id']) return Iface.delete(ifaces, background) @classmethod def from_ip(cls, ip): """Retrieve ip id associated to an ip.""" ips = dict([(ip_['ip'], ip_['id']) for ip_ in cls.list({'items_per_page': 500})]) return ips.get(ip) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be ip or id.""" try: # id is maybe an ip qry_id = cls.from_ip(id) if not qry_id: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id class Vlan(GandiModule): """ Module to handle CLI commands. $ gandi vlan list $ gandi vlan info $ gandi vlan create $ gandi vlan update $ gandi vlan delete """ @classmethod def list(cls, datacenter=None): """List virtual machine vlan (in the future it should also handle PaaS vlan).""" options = {} if datacenter: datacenter_id = int(Datacenter.usable_id(datacenter)) options['datacenter_id'] = datacenter_id return cls.call('hosting.vlan.list', options) @classmethod def resource_list(cls): """ Get the possible list of resources (name, id). """ items = cls.list() ret = [vlan['name'] for vlan in items] ret.extend([str(vlan['id']) for vlan in items]) return ret @classmethod def ifaces(cls, name): """ Get vlan attached ifaces. """ ifaces = Iface.list({'vlan_id': cls.usable_id(name)}) ret = [] for iface in ifaces: ret.append(Iface.info(iface['id'])) return ret @classmethod def _info(cls, vlan_id): """ Get information about a vlan.""" return cls.call('hosting.vlan.info', vlan_id) @classmethod def info(cls, name): """ Get information about a vlan.""" return cls._info(cls.usable_id(name)) @classmethod def delete(cls, resources, background=False): """Delete a vlan.""" if not isinstance(resources, (list, tuple)): resources = [resources] opers = [] for item in resources: oper = cls.call('hosting.vlan.delete', cls.usable_id(item)) if not oper: continue if isinstance(oper, list): opers.extend(oper) else: opers.append(oper) if background: return opers # interactive mode, run a progress bar cls.echo('Deleting your vlan.') if opers: cls.display_progress(opers) @classmethod def create(cls, name, datacenter, subnet=None, gateway=None, background=False): """Create a new vlan.""" if not background and not cls.intty(): background = True datacenter_id_ = int(Datacenter.usable_id(datacenter)) vlan_params = { 'name': name, 'datacenter_id': datacenter_id_, } if subnet: vlan_params['subnet'] = subnet if gateway: vlan_params['gateway'] = gateway result = cls.call('hosting.vlan.create', vlan_params) if not background: # interactive mode, run a progress bar cls.echo('Creating your vlan.') cls.display_progress(result) cls.echo('Your vlan %s has been created.' % name) return result @classmethod def update(cls, id, params): """Update an existing vlan.""" cls.echo('Updating your vlan.') result = cls.call('hosting.vlan.update', cls.usable_id(id), params) return result @classmethod def from_name(cls, name): """Retrieve vlan id associated to a name.""" result = cls.list() vlans = {} for vlan in result: vlans[vlan['name']] = vlan['id'] return vlans.get(name) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be name or id.""" try: # id is maybe a name qry_id = cls.from_name(id) if not qry_id: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id class Iface(GandiModule): """ Module to handle CLI commands. $ gandi iface list $ gandi iface info $ gandi iface create $ gandi iface delete $ gandi iface update """ @classmethod def list(cls, options=None): """ List all ifaces.""" options = options or {} return cls.call('hosting.iface.list', options) @classmethod def _info(cls, iface_id): """ Get information about an iface.""" return cls.call('hosting.iface.info', iface_id) @classmethod def info(cls, num): """ Get information about an iface.""" return cls._info(cls.usable_id(num)) @classmethod def usable_id(cls, id): """ Retrieve id from input which can be num or id.""" try: qry_id = int(id) except Exception: qry_id = None if not qry_id: msg = 'unknown identifier %s' % id cls.error(msg) return qry_id @classmethod def _attach(cls, iface_id, vm_id): """ Attach an iface to a vm. """ oper = cls.call('hosting.vm.iface_attach', vm_id, iface_id) return oper @classmethod def create(cls, ip_version, datacenter, bandwidth, vlan, vm, ip, background): """ Create a new iface """ if not background and not cls.intty(): background = True datacenter_id_ = int(Datacenter.usable_id(datacenter)) iface_params = { 'ip_version': ip_version, 'datacenter_id': datacenter_id_, 'bandwidth': bandwidth, } if vlan: iface_params['vlan'] = Vlan.usable_id(vlan) if ip: iface_params['ip'] = ip result = cls.call('hosting.iface.create', iface_params) if background and not vm: return result # interactive mode, run a progress bar cls.echo('Creating your iface.') cls.display_progress(result) iface_info = cls._info(result['iface_id']) cls.echo('Your iface has been created with the following IP ' 'addresses:') for _ip in iface_info['ips']: cls.echo('ip%d:\t%s' % (_ip['version'], _ip['ip'])) if not vm: return result vm_id = Iaas.usable_id(vm) result = cls._attach(result['iface_id'], vm_id) if background: return result cls.echo('Attaching your iface.') cls.display_progress(result) return result @classmethod def _detach(cls, iface_id): """ Detach an iface from a vm. """ iface = cls._info(iface_id) opers = [] vm_id = iface.get('vm_id') if vm_id: cls.echo('The iface is still attached to the vm %s.' % vm_id) cls.echo('Will detach it.') opers.append(cls.call('hosting.vm.iface_detach', vm_id, iface_id)) return opers @classmethod def delete(cls, resources, background=False): """ Delete this iface.""" if not isinstance(resources, (list, tuple)): resources = [resources] resources = [cls.usable_id(item) for item in resources] opers = [] for iface_id in resources: opers.extend(cls._detach(iface_id)) if opers: cls.echo('Detaching your iface(s).') cls.display_progress(opers) opers = [] for iface_id in resources: oper = cls.call('hosting.iface.delete', iface_id) opers.append(oper) if background: return opers cls.echo('Detaching/deleting your iface(s).') cls.display_progress(opers) return opers @classmethod def update(cls, id, bandwidth, vm, background): """ Update this iface. """ if not background and not cls.intty(): background = True iface_params = {} iface_id = cls.usable_id(id) if bandwidth: iface_params['bandwidth'] = bandwidth if iface_params: result = cls.call('hosting.iface.update', iface_id, iface_params) if background: return result # interactive mode, run a progress bar cls.echo('Updating your iface %s.' % id) cls.display_progress(result) if not vm: return vm_id = Iaas.usable_id(vm) opers = cls._detach(iface_id) if opers: cls.echo('Detaching iface.') cls.display_progress(opers) result = cls._attach(iface_id, vm_id) if background: return result cls.echo('Attaching your iface.') cls.display_progress(result) gandi.cli-1.2/gandi/cli/tests/0000755000175000017500000000000013227415174017063 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/__init__.py0000644000175000017500000000000012453203306021152 0ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/commands/0000755000175000017500000000000013227415174020664 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/commands/test_oper.py0000644000175000017500000000213413164644453023246 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from .base import CommandTestCase from gandi.cli.commands import oper class OperTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(oper.list, []) self.assertEqual(result.output, """\ id : 99002 type : hosting_migration_vm step : RUN ---------- id : 99001 type : hosting_migration_vm step : RUN ---------- id : 100303 type : certificate_update step : WAIT ---------- id : 100302 type : certificate_update step : RUN ---------- id : 100300 type : certificate_update step : RUN ---------- id : 100200 type : billing_prepaid_add_money step : BILL ---------- id : 100100 type : domain_renew step : BILL """) self.assertEqual(result.exit_code, 0) def test_info(self): result = self.invoke_with_exceptions(oper.info, ['100100']) self.assertEqual(result.output, """\ id : 100100 type : domain_renew step : BILL last_error: """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_snapshotprofile.py0000644000175000017500000000366312623134755025527 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from .base import CommandTestCase from gandi.cli.commands import snapshotprofile class SnapshotprofileTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(snapshotprofile.list, []) self.assertEqual(result.output, """\ id : 1 name : minimal kept_total : 2 target : vm ---------- id : 2 name : full_week kept_total : 7 target : vm ---------- id : 3 name : security kept_total : 10 target : vm ---------- id : 7 name : paas_normal kept_total : 3 target : paas """) self.assertEqual(result.exit_code, 0) def test_list_filter_paas(self): args = ['--only-paas'] result = self.invoke_with_exceptions(snapshotprofile.list, args) self.assertEqual(result.output, """\ id : 7 name : paas_normal kept_total : 3 target : paas """) self.assertEqual(result.exit_code, 0) def test_list_filter_vm(self): args = ['--only-vm'] result = self.invoke_with_exceptions(snapshotprofile.list, args) self.assertEqual(result.output, """\ id : 1 name : minimal kept_total : 2 target : vm ---------- id : 2 name : full_week kept_total : 7 target : vm ---------- id : 3 name : security kept_total : 10 target : vm """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['security'] result = self.invoke_with_exceptions(snapshotprofile.info, args) self.assertEqual(result.output, """\ id : 3 name : security kept_total : 10 target : vm quota_factor : 2.0 ---------- name : hourly6 kept_version : 3 ---------- name : daily kept_version : 6 ---------- name : weekly4 kept_version : 1 """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_config.py0000644000175000017500000000566412623134755023557 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- """ Configuration namespace tests. """ import os from .base import CommandTestCase from gandi.cli.commands import config class ConfigTestCase(CommandTestCase): def test_get_empty(self): result = self.invoke_with_exceptions(config.get, []) self.assertEqual(result.exit_code, 2) def test_get_unknown(self): result = self.invoke_with_exceptions(config.get, ['unknown-key']) self.assertEqual(result.output, """No value found. """) self.assertEqual(result.exit_code, 1) def test_get(self): result = self.invoke_with_exceptions(config.get, ['api']) self.assertEqual(result.exit_code, 0) def test_set_empty(self): result = self.invoke_with_exceptions(config.set, []) self.assertEqual(result.exit_code, 2) result = self.invoke_with_exceptions(config.set, ['some-key']) self.assertEqual(result.exit_code, 2) def test_set_empty_value(self): result = self.invoke_with_exceptions(config.set, ['some-key']) self.assertEqual(result.exit_code, 2) def test_set_get(self): result = self.invoke_with_exceptions(config.set, ['dummy']) self.assertEqual(result.exit_code, 2) result = self.invoke_with_exceptions(config.set, ['dummy', 'v4lu3']) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(config.get, ['dummy']) self.assertEqual(result.output, """v4lu3 """) self.assertEqual(result.exit_code, 0) def test_delete_empty(self): result = self.invoke_with_exceptions(config.set, []) self.assertEqual(result.exit_code, 2) def test_delete(self): result = self.invoke_with_exceptions(config.set, ['dummy', 'value']) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(config.get, ['dummy']) self.assertEqual(result.output, """value """) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(config.delete, ['dummy']) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(config.get, ['unknown-key']) self.assertEqual(result.output, """No value found. """) self.assertEqual(result.exit_code, 1) def test_edit(self): os.environ['EDITOR'] = '/usr/bin/nano' result = self.invoke_with_exceptions(config.get, ['editor']) self.assertEqual(result.output, """/usr/bin/nano """) self.assertEqual(result.exit_code, 0) del os.environ['EDITOR'] result = self.invoke_with_exceptions(config.set, ['editor', '/usr/bin/vi']) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(config.get, ['editor']) self.assertEqual(result.output, """/usr/bin/vi """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_status.py0000644000175000017500000001753113160664756023637 0ustar sayounsayoun00000000000000import json from functools import partial from ..compat import mock from .base import CommandTestCase from gandi.cli.commands import root # disable SSL requests warning for tests import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() RESPONSES = { 'https://status.gandi.net/api/status/schema': { 'status': 200, 'headers': 'application/json', 'body': """{"fields": {"status": {"value": [ {"SUNNY": "All services are up and running"}, {"CLOUDY": "A scheduled maintenance ongoing"}, {"FOGGY": "Incident which are not impacting our services."}, {"STORMY": "An incident ongoing"}]}}}""" }, } def _mock_requests(status, method, url, *args, **kwargs): content = None if status == 'SUNNY': if url == 'https://status.gandi.net/api/services': content = """[{"description": "IAAS", "name": "IAAS", "status": "SUNNY"}, {"description": "PAAS", "name": "PAAS", "status": "SUNNY"}, {"description": "Site", "name": "Site", "status": "SUNNY"}, {"description": "API", "name": "API", "status": "SUNNY"}, {"description": "SSL", "name": "SSL", "status": "SUNNY"}, {"description": "Domain", "name": "Domain", "status": "SUNNY"}, {"description": "Email", "name": "Email", "status": "SUNNY"}]""" if url == 'https://status.gandi.net/api/events?category=Incident¤t=true': # noqa content = """[]""" if status == 'STORMY': if url == 'https://status.gandi.net/api/services': content = """[{"description": "IAAS", "name": "IAAS", "status": "SUNNY"}, {"description": "PAAS", "name": "PAAS", "status": "STORMY"}, {"description": "Site", "name": "Site", "status": "SUNNY"}, {"description": "API", "name": "API", "status": "SUNNY"}, {"description": "SSL", "name": "SSL", "status": "SUNNY"}, {"description": "Domain", "name": "Domain", "status": "SUNNY"}, {"description": "Email", "name": "Email", "status": "SUNNY"}]""" if url == 'https://status.gandi.net/api/events?category=Incident&services=PAAS¤t=true': # noqa content = """ [{"category": "Incident", "date_end": "2014-10-08T06:20:00+00:00", "date_start": "2014-10-07T18:00:00+00:00", "duration": 740, "estimate_date_end": "2014-10-08T06:20:00+00:00", "id": "7", "services": [ "IAAS", "PAAS" ], "title": "Incident on a storage unit on Paris datacenter"}] """ if status == 'FOGGY': if url == 'https://status.gandi.net/api/services': content = """[{"description": "IAAS", "name": "IAAS", "status": "SUNNY"}, {"description": "PAAS", "name": "PAAS", "status": "SUNNY"}, {"description": "Site", "name": "Site", "status": "SUNNY"}, {"description": "API", "name": "API", "status": "SUNNY"}, {"description": "SSL", "name": "SSL", "status": "SUNNY"}, {"description": "Domain", "name": "Domain", "status": "SUNNY"}, {"description": "Email", "name": "Email", "status": "SUNNY"}]""" if url == 'https://status.gandi.net/api/events?category=Incident¤t=true': # noqa content = """ [{"category": "Incident", "date_end": "2015-04-15T22:06:43+00:00", "date_start": "2015-04-15T21:30:00+00:00", "duration": 36, "estimate_date_end": "2015-04-15T22:30:00+00:00", "id": "15", "services": [], "title": "Reachability issue on our website" }] """ if url == 'https://status.gandi.net/api/status': content = """{"status": "%s"}""" % status elif not content: content = RESPONSES[url]['body'] content = json.loads(content) mock_resp = mock.Mock() mock_resp.status_code = 200 mock_resp.content = content mock_resp.json = mock.Mock(return_value=content) return mock_resp class StatusTestCase(CommandTestCase): @mock.patch('gandi.cli.core.client.requests.request') def test_status(self, mock_request): mock_request.side_effect = partial(_mock_requests, 'SUNNY') result = self.invoke_with_exceptions(root.status, []) wanted = ("""\ IAAS : All services are up and running PAAS : All services are up and running Site : All services are up and running API : All services are up and running SSL : All services are up and running Domain : All services are up and running Email : All services are up and running """) self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_status_service(self, mock_request): mock_request.side_effect = partial(_mock_requests, 'SUNNY') result = self.invoke_with_exceptions(root.status, ['ssl']) wanted = ("""\ SSL : All services are up and running """) self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_status_service_incident(self, mock_request): mock_request.side_effect = partial(_mock_requests, 'STORMY') result = self.invoke_with_exceptions(root.status, ['paas']) url = 'https://status.gandi.net/timeline/events/7' wanted = ("""\ PAAS : Incident on a storage unit on Paris datacenter - %s """) % url self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_status_no_service_incident(self, mock_request): mock_request.side_effect = partial(_mock_requests, 'FOGGY') result = self.invoke_with_exceptions(root.status, []) wanted = ("""\ Reachability issue on our website - https://status.gandi.net/timeline/events/15 IAAS : All services are up and running PAAS : All services are up and running Site : All services are up and running API : All services are up and running SSL : All services are up and running Domain : All services are up and running Email : All services are up and running """) self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_domain.py0000644000175000017500000002161212714035303023536 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- import re from datetime import datetime from .base import CommandTestCase from ..compat import mock from gandi.cli.commands import domain from gandi.cli.core.utils import DomainNotAvailable class DomainTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(domain.list, []) self.assertEqual(result.output, """iheartcli.com cli.sexy """) self.assertEqual(result.exit_code, 0) def test_info(self): with mock.patch('gandi.cli.core.utils.datetime') as mock_datetime: mock_datetime.now.return_value = datetime(2015, 7, 1) mock_datetime.side_effect = lambda *args, **kw: datetime(*args, **kw) result = self.invoke_with_exceptions(domain.info, ['iheartcli.com']) self.assertEqual(result.output, """owner : AA1-GANDI admin : AA2-GANDI bill : AA3-GANDI tech : AA5-GANDI reseller : AA4-GANDI fqdn : iheartcli.com nameservers : a.dns.gandi.net, b.dns.gandi.net, c.dns.gandi.net services : gandidns zone_id : 424242 tags : bla created : 2010-09-22 15:06:18 expires : 2015-09-22 00:00:00 (in 83 days) updated : 2014-09-21 03:10:07 """) self.assertEqual(result.exit_code, 0) def test_create_no_option_no_argument(self): args = ['--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ] result = self.invoke_with_exceptions(domain.create, args, input='idontlike.website\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Name of the domain: idontlike.website Creating your domain. \rProgress: [###] 100.00% 00:00:00 \n\ Your domain idontlike.website has been created.""") self.assertEqual(result.exit_code, 0) # self.assertEqual(result.output.strip(), """\ # Name of the domain.:\ # """) def test_create_option(self): result = self.invoke_with_exceptions(domain.create, ['--domain', 'idontlike.website', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', '--nameserver', 'a.domain.tld', '--nameserver', 'b.domain.tld', ]) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ /!\ --domain option is deprecated and will be removed upon next release. You should use 'gandi domain create idontlike.website' instead. Creating your domain. \rProgress: [###] 100.00% 00:00:00 \n\ Your domain idontlike.website has been created.""") self.assertEqual(result.exit_code, 0) def test_create_argument(self): result = self.invoke_with_exceptions(domain.create, ['idontlike.website', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ]) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Creating your domain. \rProgress: [###] 100.00% 00:00:00 \n\ Your domain idontlike.website has been created.""") self.assertEqual(result.exit_code, 0) def test_create_argument_and_option_different(self): result = self.invoke_with_exceptions(domain.create, ['idontlike.website', '--domain', 'idontlike.bike', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ]) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ /!\ --domain option is deprecated and will be removed upon next release. You should use 'gandi domain create idontlike.bike' instead. /!\ You specified both an option and an argument which are different, \ please choose only one between: idontlike.bike and idontlike.website.""") self.assertEqual(result.exit_code, 0) def test_create_argument_and_option_equal(self): result = self.invoke_with_exceptions(domain.create, ['idontlike.website', '--domain', 'idontlike.website', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ]) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ /!\ --domain option is deprecated and will be removed upon next release. You should use 'gandi domain create idontlike.website' instead. Creating your domain. \rProgress: [###] 100.00% 00:00:00 \n\ Your domain idontlike.website has been created.""") self.assertEqual(result.exit_code, 0) def test_create_background_argument(self): args = ['roflozor.com', '--background'] result = self.invoke_with_exceptions(domain.create, args) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Duration [1]: \n\ {'id': 400, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) def test_create_background_option(self): args = ['--domain', 'roflozor.com', '--background'] result = self.invoke_with_exceptions(domain.create, args) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Duration [1]: \n\ /!\ --domain option is deprecated and will be removed upon next release. You should use 'gandi domain create roflozor.com' instead. {'id': 400, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) def test_renew(self): result = self.invoke_with_exceptions(domain.renew, ['iheartcli.com', '--duration', 1, ]) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Renewing your domain. \rProgress: [###] 100.00% 00:00:00 \n\ Your domain iheartcli.com has been renewed.""") self.assertEqual(result.exit_code, 0) def test_renew_background_ok(self): args = ['iheartcli.com', '--background'] result = self.invoke_with_exceptions(domain.renew, args) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Duration [1]: \n\ {'id': 400, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) def test_available_with_exception(self): self.assertRaises(DomainNotAvailable, self.invoke_with_exceptions, domain.create, ['--domain', 'unavailable1.website', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ]) def test_available_with_exception_argument(self): self.assertRaises(DomainNotAvailable, self.invoke_with_exceptions, domain.create, ['unavailable1.website', '--duration', 1, '--owner', 'OWNER1-GANDI', '--admin', 'ADMIN1-GANDI', '--tech', 'TECH1-GANDI', '--bill', 'BILL1-GANDI', ]) gandi.cli-1.2/gandi/cli/tests/commands/test_vlan.py0000644000175000017500000002356213227142754023246 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- import re from .base import CommandTestCase from gandi.cli.commands import vlan from gandi.cli.core.base import GandiContextHelper class VlanTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(vlan.list, []) self.assertEqual(result.output, """\ name : vlantest state : created datacenter: FR-SD2 ---------- name : pouet state : created datacenter: FR-SD2 ---------- name : intranet state : created datacenter: FR-SD3 """) self.assertEqual(result.exit_code, 0) def test_list_filters(self): args = ['--id', '--subnet', '--gateway'] result = self.invoke_with_exceptions(vlan.list, args) self.assertEqual(result.output, """\ name : vlantest state : created id : 123 subnet : 10.7.13.0/24 gateway : 10.7.13.254 datacenter: FR-SD2 ---------- name : pouet state : created id : 717 subnet : 192.168.232.0/24 gateway : 192.168.232.254 datacenter: FR-SD2 ---------- name : intranet state : created id : 999 subnet : 10.7.242.0/24 gateway : 10.7.242.254 datacenter: FR-SD3 """) self.assertEqual(result.exit_code, 0) def test_list_filter_datacenter(self): args = ['--id', '--subnet', '--gateway', '--datacenter', 'FR-SD3'] result = self.invoke_with_exceptions(vlan.list, args) self.assertEqual(result.output, """\ name : intranet state : created id : 999 subnet : 10.7.242.0/24 gateway : 10.7.242.254 datacenter: FR-SD3 """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['vlantest'] result = self.invoke_with_exceptions(vlan.info, args) self.assertEqual(result.output, """\ name : vlantest state : created subnet : 10.7.13.0/24 gateway : 10.7.13.254 datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_info_ip(self): args = ['pouet', '--ip'] result = self.invoke_with_exceptions(vlan.info, args) self.assertEqual(result.output, """\ name : pouet state : created subnet : 192.168.232.0/24 gateway : 192.168.232.254 don't exists datacenter : FR-SD2 ---------- bandwidth : 102400.0 vm : server02 ip : 192.168.232.252 ---------- bandwidth : 204800.0 vm : server02 ip : 192.168.232.253 """) self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['intranet'] result = self.invoke_with_exceptions(vlan.delete, args, input='y\n', obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete vlan 'intranet'? [y/N]: y Deleting your vlan. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_force(self): args = ['intranet', '--force'] result = self.invoke_with_exceptions(vlan.delete, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Deleting your vlan. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background(self): args = ['intranet', '--force', '--bg'] result = self.invoke_with_exceptions(vlan.delete, args, obj=GandiContextHelper()) self.assertEqual(result.output, """\ id : 200 step : WAIT """) self.assertEqual(result.exit_code, 0) def test_delete_unknown(self): args = ['vlanunknown'] result = self.invoke_with_exceptions(vlan.delete, args) self.assertEqual(result.output, """\ Sorry vlan vlanunknown does not exist Please use one of the following: ['vlantest', 'pouet', 'intranet', \ '123', '717', '999'] """) self.assertEqual(result.exit_code, 0) def test_delete_refuse(self): args = ['intranet'] result = self.invoke_with_exceptions(vlan.delete, args, input='\n', obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete vlan 'intranet'? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_create(self): args = ['--name', 'testvlan', '--datacenter', 'FR-SD3', '--subnet', '10.7.70.0/24', '--gateway', '10.7.70.254'] result = self.invoke_with_exceptions(vlan.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your vlan. \rProgress: [###] 100.00% 00:00:00 \ \nYour vlan testvlan has been created.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.vlan.create'][0][0] self.assertEqual(params['datacenter_id'], 4) self.assertEqual(params['subnet'], '10.7.70.0/24') self.assertEqual(params['name'], 'testvlan') self.assertEqual(params['gateway'], '10.7.70.254') def test_create_datacenter_limited(self): args = ['--name', 'testvlan', '--datacenter', 'FR-SD2', '--subnet', '10.7.70.0/24', '--gateway', '10.7.70.254'] result = self.invoke_with_exceptions(vlan.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider using \ another datacenter. Creating your vlan. \rProgress: [###] 100.00% 00:00:00 \ \nYour vlan testvlan has been created.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.vlan.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['subnet'], '10.7.70.0/24') self.assertEqual(params['name'], 'testvlan') self.assertEqual(params['gateway'], '10.7.70.254') def test_create_datacenter_closed(self): args = ['--name', 'testvlan', '--datacenter', 'US-BA1', '--subnet', '10.7.70.0/24', '--gateway', '10.7.70.254'] result = self.invoke_with_exceptions(vlan.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_create_background(self): args = ['--name', 'testvlanbg', '--bg'] result = self.invoke_with_exceptions(vlan.create, args, obj=GandiContextHelper()) self.assertEqual(result.output.strip(), """\ {'id': 200, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.vlan.create'][0][0] self.assertEqual(params['datacenter_id'], 5) self.assertEqual(params['name'], 'testvlanbg') def test_update(self): args = ['pouet', '--name', 'chocolat', '--gateway', '10.7.70.254', '--bandwidth', '204800'] result = self.invoke_with_exceptions(vlan.update, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your vlan.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.vlan.update'][0][1] self.assertEqual(params['name'], 'chocolat') self.assertEqual(params['gateway'], '10.7.70.254') def test_update_gateway_vm_unknown(self): args = ['pouet', '--name', 'chocolat', '--gateway', 'server01', '--bandwidth', '204800'] result = self.invoke_with_exceptions(vlan.update, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Can't find 'server01' in 'pouet' vlan""") self.assertEqual(result.exit_code, 0) def test_update_gateway_vm(self): args = ['pouet', '--name', 'chocolat', '--gateway', 'server01', '--create', '--bandwidth', '204800'] result = self.invoke_with_exceptions(vlan.update, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Will create a new ip in this vlan for vm server01 Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t95.142.160.181 ip6:\t2001:4b98:dc0:47:216:3eff:feb2:3862 Attaching your iface. \rProgress: [###] 100.00% 00:00:00 \ \nUpdating your vlan.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.vlan.update'][0][1] self.assertEqual(params['name'], 'chocolat') self.assertEqual(params['gateway'], '95.142.160.181') def test_update_gateway_multiple_ips(self): args = ['pouet', '--name', 'chocolat', '--gateway', 'server02'] result = self.invoke_with_exceptions(vlan.update, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ This vm has two ips in the vlan, don't know which one to choose \ (213.167.231.3, 192.168.232.252)""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_record.py0000644000175000017500000002444513227414205023556 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from ..compat import mock from .base import CommandTestCase from gandi.cli.commands import record class RecordTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(record.list, ['iheartcli.com']) self.assertEqual(result.output, """\ name : * type : A value : 73.246.104.110 ttl : 10800 ---------- name : @ type : A value : 73.246.104.110 ttl : 10800 ---------- name : much type : A value : 192.243.24.132 ttl : 10800 ---------- name : blog type : CNAME value : blogs.vip.gandi.net. ttl : 10800 ---------- name : cloud type : CNAME value : gpaas6.dc0.gandi.net. ttl : 10800 ---------- name : imap type : CNAME value : access.mail.gandi.net. ttl : 10800 ---------- name : pop type : CNAME value : access.mail.gandi.net. ttl : 10800 ---------- name : smtp type : CNAME value : relay.mail.gandi.net. ttl : 10800 ---------- name : webmail type : CNAME value : agent.mail.gandi.net. ttl : 10800 ---------- name : @ type : MX value : 50 fb.mail.gandi.net. ttl : 10800 ---------- name : @ type : MX value : 10 spool.mail.gandi.net. ttl : 10800 """) self.assertEqual(result.exit_code, 0) def test_list_format_text(self): args = ['iheartcli.com', '--format', 'text'] result = self.invoke_with_exceptions(record.list, args) self.assertEqual(result.output, """\ * 10800 IN A 73.246.104.110 @ 10800 IN A 73.246.104.110 much 10800 IN A 192.243.24.132 blog 10800 IN CNAME blogs.vip.gandi.net. cloud 10800 IN CNAME gpaas6.dc0.gandi.net. imap 10800 IN CNAME access.mail.gandi.net. pop 10800 IN CNAME access.mail.gandi.net. smtp 10800 IN CNAME relay.mail.gandi.net. webmail 10800 IN CNAME agent.mail.gandi.net. @ 10800 IN MX 50 fb.mail.gandi.net. @ 10800 IN MX 10 spool.mail.gandi.net. """) self.assertEqual(result.exit_code, 0) def test_list_format_json(self): args = ['iheartcli.com', '--format', 'json'] result = self.invoke_with_exceptions(record.list, args) self.assertEqual(result.output, """\ [ { "id": 337085079, "name": "*", "ttl": 10800, "type": "A", "value": "73.246.104.110" }, { "id": 337085078, "name": "@", "ttl": 10800, "type": "A", "value": "73.246.104.110" }, { "id": 337085081, "name": "much", "ttl": 10800, "type": "A", "value": "192.243.24.132" }, { "id": 337085072, "name": "blog", "ttl": 10800, "type": "CNAME", "value": "blogs.vip.gandi.net." }, { "id": 337085082, "name": "cloud", "ttl": 10800, "type": "CNAME", "value": "gpaas6.dc0.gandi.net." }, { "id": 337085075, "name": "imap", "ttl": 10800, "type": "CNAME", "value": "access.mail.gandi.net." }, { "id": 337085071, "name": "pop", "ttl": 10800, "type": "CNAME", "value": "access.mail.gandi.net." }, { "id": 337085074, "name": "smtp", "ttl": 10800, "type": "CNAME", "value": "relay.mail.gandi.net." }, { "id": 337085073, "name": "webmail", "ttl": 10800, "type": "CNAME", "value": "agent.mail.gandi.net." }, { "id": 337085077, "name": "@", "ttl": 10800, "type": "MX", "value": "50 fb.mail.gandi.net." }, { "id": 337085076, "name": "@", "ttl": 10800, "type": "MX", "value": "10 spool.mail.gandi.net." } ] """) self.assertEqual(result.exit_code, 0) def test_list_no_zone(self): result = self.invoke_with_exceptions(record.list, ['cli.sexy']) self.assertEqual(result.output, """\ No zone records found, domain cli.sexy doesn't seems to be managed at Gandi. """) self.assertEqual(result.exit_code, 0) def test_list_output(self): args = ['iheartcli.com', '--output'] with mock.patch('gandi.cli.commands.record.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(record.list, args) self.assertEqual(result.output, """\ Your zone file have been writen in iheartcli.com_424242 """) self.assertEqual(result.exit_code, 0) def test_list_output_reset(self): args = ['iheartcli.com', '--output'] with mock.patch('gandi.cli.commands.record.os.path.isfile', create=True) as mock_isfile: mock_isfile.return_value = mock.MagicMock() with mock.patch('gandi.cli.commands.record.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(record.list, args) self.assertEqual(result.output, """\ Your zone file have been writen in iheartcli.com_424242 """) self.assertEqual(result.exit_code, 0) def test_create_no_zone(self): args = ['cli.sexy', '--name', '@', '--type', 'A', '--value', '127.0.0.1'] result = self.invoke_with_exceptions(record.create, args) self.assertEqual(result.output, """\ No zone records found, domain cli.sexy doesn't seems to be managed at Gandi. """) self.assertEqual(result.exit_code, 0) def test_create(self): args = ['iheartcli.com', '--name', '@', '--type', 'A', '--ttl', 3600, '--value', '127.0.0.1'] result = self.invoke_with_exceptions(record.create, args) self.assertEqual(result.output, """\ Creating new zone version Updating zone version Activation of new zone version """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.zone.record.add'][0][2] self.assertEqual(params['name'], '@') self.assertEqual(params['type'], 'A') self.assertEqual(params['value'], '127.0.0.1') self.assertEqual(params['ttl'], 3600) def test_delete_no_zone(self): args = ['cli.sexy'] result = self.invoke_with_exceptions(record.delete, args) self.assertEqual(result.output, """\ No zone records found, domain cli.sexy doesn't seems to be managed at Gandi. """) self.assertEqual(result.exit_code, 0) def test_delete_all_ko(self): args = ['iheartcli.com'] result = self.invoke_with_exceptions(record.delete, args, input='N\n') self.assertEqual(result.output, """\ This command without parameters --type, --name or --value will remove all \ records in this zone file. Are you sur to perform this action ? [y/N]: N """) self.assertEqual(result.exit_code, 0) def test_delete_all(self): args = ['iheartcli.com'] result = self.invoke_with_exceptions(record.delete, args, input='y\n') self.assertEqual(result.output, """\ This command without parameters --type, --name or --value will remove all \ records in this zone file. Are you sur to perform this action ? [y/N]: y Creating new zone record Deleting zone record Activation of new zone version """) self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['iheartcli.com', '--name', '@', '--type', 'A', '--value', '127.0.0.1'] result = self.invoke_with_exceptions(record.delete, args) self.assertEqual(result.output, """\ Creating new zone record Deleting zone record Activation of new zone version """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.zone.record.delete'][0][2] self.assertEqual(params['name'], '@') self.assertEqual(params['type'], 'A') self.assertEqual(params['value'], '127.0.0.1') def test_update_no_zone(self): args = ['cli.sexy'] result = self.invoke_with_exceptions(record.update, args) self.assertEqual(result.output, """\ No zone records found, domain cli.sexy doesn't seems to be managed at Gandi. """) self.assertEqual(result.exit_code, 0) def test_update_no_param(self): args = ['iheartcli.com'] result = self.invoke_with_exceptions(record.update, args) self.assertEqual(result.output, """\ You must indicate a zone file or a record. \ Use `gandi record update --help` for more information """) self.assertEqual(result.exit_code, 0) def test_update(self): args = ['iheartcli.com', '--record', '* 10800 A 73.246.104.110', '--new-record', '@ 3600 A 127.0.0.1'] result = self.invoke_with_exceptions(record.update, args) self.assertEqual(result.output, """\ Creating new zone file Updating zone records Activation of new zone version """) self.assertEqual(result.exit_code, 0) def test_update_name(self): args = ['iheartcli.com', '--record', '*', '--new-record', '@ 3600 A 127.0.0.1'] result = self.invoke_with_exceptions(record.update, args) self.assertEqual(result.output, """\ Creating new zone file Updating zone records Activation of new zone version """) self.assertEqual(result.exit_code, 0) def test_update_file(self): args = ['iheartcli.com', '--file', 'sandbox/example.txt'] content = """\ * 10800 IN A 73.246.104.110 @ 10800 IN A 73.246.104.110 much 10800 IN A 192.243.24.132 blog 10800 IN CNAME blogs.vip.gandi.net. cloud 10800 IN CNAME gpaas6.dc0.gandi.net. imap 10800 IN CNAME access.mail.gandi.net. pop 10800 IN CNAME access.mail.gandi.net. smtp 10800 IN CNAME relay.mail.gandi.net. webmail 10800 IN CNAME agent.mail.gandi.net. @ 10800 IN MX 50 fb.mail.gandi.net. @ 10800 IN MX 10 spool.mail.gandi.net. """ result = self.isolated_invoke_with_exceptions(record.update, args, temp_content=content) self.assertEqual(result.output, """\ Creating new zone file Updating zone records Activation of new zone version """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_ip.py0000644000175000017500000005263713120224646022714 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- import re from .base import CommandTestCase from gandi.cli.commands import ip from gandi.cli.core.base import GandiContextHelper class IpTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(ip.list, []) self.assertEqual(result.output, """\ ip : 95.142.160.181 state : created type : public datacenter : FR-SD2 ---------- ip : 2001:4b98:dc2:43:216:3eff:fece:e25f state : created type : public datacenter : LU-BI1 ---------- ip : 2001:4b98:dc0:47:216:3eff:feb2:3862 state : created type : public datacenter : FR-SD2 ---------- ip : 192.168.232.253 state : created type : private vlan : pouet datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created type : private vlan : pouet datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_details(self): args = ['--id', '--version', '--vm', '--reverse'] result = self.invoke_with_exceptions(ip.list, args) self.assertEqual(result.output, """\ ip : 95.142.160.181 state : created id : 203968 version : 4 reverse : xvm-160-181.dc0.ghst.net type : public vm : server01 datacenter : FR-SD2 ---------- ip : 2001:4b98:dc2:43:216:3eff:fece:e25f state : created id : 204557 version : 6 reverse : xvm6-dc2-fece-e25f.ghst.net type : public datacenter : LU-BI1 ---------- ip : 2001:4b98:dc0:47:216:3eff:feb2:3862 state : created id : 204558 version : 6 reverse : xvm6-dc0-feb2-3862.ghst.net type : public vm : server01 datacenter : FR-SD2 ---------- ip : 192.168.232.253 state : created id : 2361 version : 4 reverse : type : private vlan : pouet vm : server02 datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created id : 2361 version : 4 reverse : type : private vlan : pouet vm : server02 datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_attached(self): result = self.invoke_with_exceptions(ip.list, ['--attached']) self.assertEqual(result.output, """\ ip : 95.142.160.181 state : created type : public datacenter : FR-SD2 ---------- ip : 2001:4b98:dc0:47:216:3eff:feb2:3862 state : created type : public datacenter : FR-SD2 ---------- ip : 192.168.232.253 state : created type : private vlan : pouet datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created type : private vlan : pouet datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_detached(self): result = self.invoke_with_exceptions(ip.list, ['--detached']) self.assertEqual(result.output, """\ ip : 2001:4b98:dc2:43:216:3eff:fece:e25f state : created type : public datacenter : LU-BI1 """) self.assertEqual(result.exit_code, 0) def test_list_filter_type(self): result = self.invoke_with_exceptions(ip.list, ['--type', 'private']) self.assertEqual(result.output, """\ ip : 2001:4b98:dc2:43:216:3eff:fece:e25f state : created type : public datacenter : LU-BI1 ---------- ip : 192.168.232.253 state : created type : private vlan : pouet datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created type : private vlan : pouet datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_filter_datacenter(self): result = self.invoke_with_exceptions(ip.list, ['--datacenter', 'FR']) self.assertEqual(result.output, """\ ip : 95.142.160.181 state : created type : public datacenter : FR-SD2 ---------- ip : 2001:4b98:dc0:47:216:3eff:feb2:3862 state : created type : public datacenter : FR-SD2 ---------- ip : 192.168.232.253 state : created type : private vlan : pouet datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created type : private vlan : pouet datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_filter_vlan(self): result = self.invoke_with_exceptions(ip.list, ['--vlan', 'pouet']) self.assertEqual(result.output, """\ ip : 192.168.232.253 state : created type : private vlan : pouet datacenter : FR-SD2 ---------- ip : 192.168.232.252 state : created type : private vlan : pouet datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_list_attached_detached(self): args = ['--detached', '--attached'] result = self.invoke_with_exceptions(ip.list, args) self.assertEqual(result.output, """\ You can't set --attached and --detached at the same time. """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.info, args) self.assertEqual(result.output, """\ ip : 95.142.160.181 state : created reverse : xvm-160-181.dc0.ghst.net type : public vm : server01 datacenter : FR-SD2 """) self.assertEqual(result.exit_code, 0) def test_update_ko(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.update, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_update_reverse(self): args = ['95.142.160.181', '--reverse', 'plop.bloup.com'] result = self.invoke_with_exceptions(ip.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your IP \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_attach_ko(self): args = ['395.142.160.181', 'vm1426759833'] result = self.invoke_with_exceptions(ip.attach, args) self.assertTrue("Can't find this ip 395.142.160.181" in result.output) self.assertEqual(result.exit_code, 2) def test_attach_already(self): args = ['95.142.160.181', 'server01'] result = self.invoke_with_exceptions(ip.attach, args, input='y\n') self.assertEqual(result.output, """\ This ip is already attached to this vm. """) self.assertEqual(result.exit_code, 0) def test_attach(self): args = ['95.142.160.181', 'vm1426759833'] result = self.invoke_with_exceptions(ip.attach, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to detach 95.142.160.181 from vm 152967 [y/N]: y The iface is still attached to the vm 152967. Will detach it. \rProgress: [###] 100.00% 00:00:00 \ \n\rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_attach_force(self): args = ['95.142.160.181', 'vm1426759833', '--force'] result = self.invoke_with_exceptions(ip.attach, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The iface is still attached to the vm 152967. Will detach it. \rProgress: [###] 100.00% 00:00:00 \ \n\rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_attach_refuse(self): args = ['95.142.160.181', 'vm1426759833'] result = self.invoke_with_exceptions(ip.attach, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to detach 95.142.160.181 from vm 152967 [y/N]: N""") self.assertEqual(result.exit_code, 0) def test_attach_background(self): args = ['95.142.160.181', 'vm1426759833', '--force', '--bg'] result = self.invoke_with_exceptions(ip.attach, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The iface is still attached to the vm 152967. Will detach it. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_create_default(self): args = [] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t95.142.160.181 ip6:\t2001:4b98:dc0:47:216:3eff:feb2:3862""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 3) self.assertEqual(params['bandwidth'], 102400) self.assertEqual(params['ip_version'], 4) def test_create_datacenter_limited(self): args = ['--datacenter', 'FR-SD2', '--bandwidth', '51200', '--ip-version', '6'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t95.142.160.181 ip6:\t2001:4b98:dc0:47:216:3eff:feb2:3862""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 6) def test_create_datacenter_closed(self): args = ['--datacenter', 'US-BA1', '--bandwidth', '51200', '--ip-version', '6'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_create_params(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '6'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t95.142.160.181 ip6:\t2001:4b98:dc0:47:216:3eff:feb2:3862""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 6) def test_create_params_vlan_ko(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '6', '--vlan', 'pouet'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ You must have an --ip-version to 4 when having a vlan.""") self.assertEqual(result.exit_code, 0) def test_create_params_vlan_ok(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '4', '--vlan', 'pouet'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t95.142.160.181 ip6:\t2001:4b98:dc0:47:216:3eff:feb2:3862""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 4) self.assertEqual(params['vlan'], 717) def test_create_params_ip_ko(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '4', '--ip', '10.50.10.10'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ You must have a --vlan when giving an --ip.""") self.assertEqual(result.exit_code, 0) def test_create_params_ip_ok(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '4', '--ip', '10.50.10.10', '--vlan', 'pouet'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t10.50.10.10""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 4) self.assertEqual(params['vlan'], 717) self.assertEqual(params['ip'], '10.50.10.10') def test_create_params_attach_ko(self): args = ['--datacenter', 'US', '--bandwidth', '51200', '--ip-version', '4', '--attach', 'server01'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The datacenter you provided does not match the datacenter of the \ vm you want to attach to.""") self.assertEqual(result.exit_code, 0) def test_create_params_attach_ok(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '4', '--ip', '10.50.10.10', '--vlan', 'pouet', '--attach', 'server01'] result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t10.50.10.10 Attaching your iface. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 4) self.assertEqual(params['vlan'], 717) self.assertEqual(params['ip'], '10.50.10.10') def test_create_background(self): args = ['--datacenter', 'FR', '--bandwidth', '51200', '--ip-version', '4', '--ip', '10.50.10.10', '--vlan', 'pouet', '--attach', 'server01', '--background'] self.maxDiff = None result = self.invoke_with_exceptions(ip.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR will be closed on 25/12/2017, please consider using \ another datacenter. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \ \nYour iface has been created with the following IP addresses: ip4:\t10.50.10.10""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.iface.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['bandwidth'], 51200) self.assertEqual(params['ip_version'], 4) self.assertEqual(params['vlan'], 717) self.assertEqual(params['ip'], '10.50.10.10') def test_detach(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.detach, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to detach ip 95.142.160.181? [y/N]: y\ \nThe iface is still attached to the vm 152967. Will detach it. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_detach_refuse(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.detach, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to detach ip 95.142.160.181? [y/N]: N""") self.assertEqual(result.exit_code, 0) def test_detach_force(self): args = ['95.142.160.181', '--force'] result = self.invoke_with_exceptions(ip.detach, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The iface is still attached to the vm 152967. Will detach it. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_unknown(self): args = ['395.142.160.181'] result = self.invoke_with_exceptions(ip.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Sorry interface 395.142.160.181 does not exist Please use one of the following: ['203968', '204557', '204558', '2361', \ '2361', '95.142.160.181', '2001:4b98:dc2:43:216:3eff:fece:e25f', \ '2001:4b98:dc0:47:216:3eff:feb2:3862', '192.168.232.253', '192.168.232.252'\ ]""") self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.delete, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to delete ip(s) 95.142.160.181 [y/N]: y The iface is still attached to the vm 152967. Will detach it. Detaching your iface(s). \rProgress: [###] 100.00% 00:00:00 \ \nDetaching/deleting your iface(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_refuse(self): args = ['95.142.160.181'] result = self.invoke_with_exceptions(ip.delete, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to delete ip(s) 95.142.160.181 [y/N]: N""") self.assertEqual(result.exit_code, 0) def test_delete_force(self): args = ['95.142.160.181', '--force'] result = self.invoke_with_exceptions(ip.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The iface is still attached to the vm 152967. Will detach it. Detaching your iface(s). \rProgress: [###] 100.00% 00:00:00 \ \nDetaching/deleting your iface(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background(self): args = ['95.142.160.181', '--background'] result = self.invoke_with_exceptions(ip.delete, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to delete ip(s) 95.142.160.181 [y/N]: y The iface is still attached to the vm 152967. Will detach it. Detaching your iface(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_multiple(self): args = ['95.142.160.181', '2001:4b98:dc2:43:216:3eff:fece:e25f'] result = self.invoke_with_exceptions(ip.delete, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to delete ip(s) 2001:4b98:dc2:43:216:3eff:fece:e25f, \ 95.142.160.181 [y/N]: y The iface is still attached to the vm 152967. Will detach it. Detaching your iface(s). \rProgress: [###] 100.00% 00:00:00 \ \nDetaching/deleting your iface(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_root.py0000644000175000017500000000052012507007717023255 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from .base import CommandTestCase from gandi.cli.commands import root class RootTestCase(CommandTestCase): def test_api(self): result = self.invoke_with_exceptions(root.api, []) self.assertEqual(result.output, """\ API version: 3.3.42 """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/base.py0000644000175000017500000000441413155510475022153 0ustar sayounsayoun00000000000000import os from click.testing import CliRunner from gandi.cli.core.base import GandiModule from ..compat import unittest, mock from ..fixtures.api import Api from ..fixtures.mocks import MockObject class CommandTestCase(unittest.TestCase): base_mocks = [ ('gandi.cli.core.base.GandiModule.save', MockObject.blank_func), ('gandi.cli.core.base.GandiModule.execute', MockObject.execute), ('gandi.cli.core.base.GandiModule.deprecated', MockObject.deprecated), ] mocks = [] def setUp(self): self.runner = CliRunner() self.mocks = self.mocks + self.base_mocks self.mocks = [mock.patch(*mock_args) for mock_args in self.mocks] for dummy in self.mocks: dummy.start() GandiModule._api = Api() GandiModule._api._calls = {} GandiModule._conffiles = {'global': {'api': {'env': 'test', 'key': 'apikey0001'}, 'apirest': {'key': 'apikey002'}}} GandiModule._poll_freq = 0.1 self.api_calls = GandiModule._api._calls def tearDown(self): GandiModule._api = None GandiModule._conffiles = {} for dummy in reversed(self.mocks): dummy.stop() def invoke_with_exceptions(self, cli, args, catch_exceptions=False, **kwargs): return self.runner.invoke(cli, args, catch_exceptions=catch_exceptions, **kwargs) def isolated_invoke_with_exceptions(self, cli, args, catch_exceptions=False, temp_dir=None, temp_name=None, temp_content=None, **kwargs): temp_dir = temp_dir or 'sandbox' temp_name = temp_name or 'example.txt' with self.runner.isolated_filesystem(): os.mkdir(temp_dir) with open('%s/%s' % (temp_dir, temp_name), 'w') as f: f.write(temp_content) return self.runner.invoke(cli, args, catch_exceptions=catch_exceptions, **kwargs) gandi.cli-1.2/gandi/cli/tests/commands/test_certstore.py0000644000175000017500000001627312623134755024322 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from ..compat import mock from .base import CommandTestCase from gandi.cli.commands import certstore class CertStoreTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(certstore.list, []) self.assertEqual(result.output, """\ subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr ---------- subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr ---------- subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test2.domain.fr ---------- subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test3.domain.fr ---------- subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr ---------- subject : /OU=Domain Control Validated/OU=Gandi Standard Wildcard SSL/CN=*.domain.fr """) self.assertEqual(result.exit_code, 0) def test_list_all(self): result = self.invoke_with_exceptions(certstore.list, ['--id', '--vhosts', '--dates', '--fqdns']) self.assertEqual(result.output, """\ id : 1 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr date_created: 20150407T00:00:00 date_expire : 20160316T00:00:00 ---------- fqdn : test1.domain.fr ---------- vhost : test1.domain.fr type : paas ---------- id : 2 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr date_created: 20150407T00:00:00 date_expire : 20160316T00:00:00 ---------- fqdn : test1.domain.fr ---------- vhost : test1.domain.fr type : paas ---------- id : 3 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test2.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test2.domain.fr ---------- id : 4 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test3.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test3.domain.fr ---------- id : 5 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test4.domain.fr ---------- id : 6 subject : /OU=Domain Control Validated/OU=Gandi Standard Wildcard SSL/CN=*.domain.fr date_created: 20150409T00:00:00 date_expire : 20160409T00:00:00 ---------- fqdn : *.domain.fr ---------- vhost : *.domain.fr type : paas """) self.assertEqual(result.exit_code, 0) def test_info_fqdn(self): result = self.invoke_with_exceptions(certstore.info, ['test1.domain.fr']) self.assertEqual(result.output, """\ id : 1 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr date_created: 20150407T00:00:00 date_expire : 20160316T00:00:00 ---------- fqdn : test1.domain.fr ---------- vhost : test1.domain.fr type : paas ---------- id : 2 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr date_created: 20150407T00:00:00 date_expire : 20160316T00:00:00 ---------- fqdn : test1.domain.fr ---------- vhost : test1.domain.fr type : paas """) self.assertEqual(result.exit_code, 0) def test_info_id(self): result = self.invoke_with_exceptions(certstore.info, ['1']) self.assertEqual(result.output, """\ id : 1 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr date_created: 20150407T00:00:00 date_expire : 20160316T00:00:00 ---------- fqdn : test1.domain.fr ---------- vhost : test1.domain.fr type : paas """) self.assertEqual(result.exit_code, 0) def test_create(self): result = self.invoke_with_exceptions(certstore.create, ['--pk', 'PK', '--crt', 'CRT']) self.assertEqual(result.output, """\ id : 5 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test4.domain.fr """) self.assertEqual(result.exit_code, 0) def test_create_id(self): result = self.invoke_with_exceptions(certstore.create, ['--pk', 'PK', '--crt-id', '701']) self.assertEqual(result.output, """\ id : 5 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test4.domain.fr """) self.assertEqual(result.exit_code, 0) def test_create_missing(self): result = self.invoke_with_exceptions(certstore.create, ['--pk', 'PK']) self.assertEqual(result.output, """\ One of --certificate or --certificate-id is needed. """) self.assertEqual(result.exit_code, 0) def test_create_too_many(self): args = ['--pk', 'PK', '--crt', 'CRT', '--crt-id', '999'] result = self.invoke_with_exceptions(certstore.create, args) self.assertEqual(result.output, """\ Only one of --certificate or --certificate-id is needed. id : 5 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test4.domain.fr """) self.assertEqual(result.exit_code, 0) def test_create_parameter_files(self): args = ['--pk', '/tmp/pk.key', '--crt', '/tmp/key.crt'] with mock.patch('gandi.cli.commands.certstore.os.path.isfile', create=True) as mock_isfile: mock_isfile.return_value = True with mock.patch('gandi.cli.commands.certstore.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certstore.create, args) self.assertEqual(result.output, """\ id : 5 subject : /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test4.domain.fr date_created: 20150408T00:00:00 date_expire : 20160408T00:00:00 ---------- fqdn : test4.domain.fr """) self.assertEqual(result.exit_code, 0) def test_delete(self): result = self.invoke_with_exceptions(certstore.delete, ['1']) self.assertEqual(result.output, """\ Are you sure to delete the following hosted certificates ? 1: /OU=Domain Control Validated/OU=Gandi Standard SSL/CN=test1.domain.fr [y/N]: \ """) self.assertEqual(result.exit_code, 0) result = self.invoke_with_exceptions(certstore.delete, ['1', '-f']) self.assertEqual(result.output, """\ """) self.assertEqual(result.exit_code, 0) def test_delete_unknown(self): args = ['100.fr', '-f'] result = self.invoke_with_exceptions(certstore.delete, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_contact.py0000644000175000017500000001323312623134755023734 0ustar sayounsayoun00000000000000import re from .base import CommandTestCase from ..fixtures.mocks import MockObject from gandi.cli.core.base import GandiModule from gandi.cli.commands import contact class ContactTestCase(CommandTestCase): mocks = [('gandi.cli.commands.contact.webbrowser.open', MockObject.blank_func)] def test_create_dry_run_ok(self): args = [] inputs = ('0\nPeter\nParker\npeter.parker@spiderman.org\n' 'Central Park\n2600\nNew York\nUSA\n555-123-456\n' 'plokiploki\nplokiploki\n+011.555123456\napikey0001\n') result = self.invoke_with_exceptions(contact.create, args, input=inputs) self.assertEqual(re.sub(r'\[\d+\]', '[1234567890123456]', result.output.strip()), """\ Choose your contact type 0- individual 1- company 2- association 3- public body 4- reseller : 0 What is your first name: Peter What is your last name: Parker What is your email address: peter.parker@spiderman.org What is your street address: Central Park What is your zipcode: 2600 Which city: New York Which country: USA What is your telephone number: 555-123-456 Please enter your password [1234567890123456]: \ \nRepeat for confirmation: \ \nphone: string '555-123-456' does not match '^\\+\\d{1,3}\\.\\d+$' What is your telephone number: +011.555123456 Please activate you public api access from gandi website, and get the apikey. Your handle is PP0000-GANDI, and the password is the one you defined. What is your production apikey: apikey0001 You already have an apikey defined, if you want to use the newly created \ contact, use the env var : \ \nexport API_KEY=apikey0001""") self.assertEqual(result.exit_code, 0) def test_create_dry_run_unknown_ok(self): args = [] inputs = ('0\nPeter\nParker\ngreen.goblin@spiderman.org\n' 'Central Park\n2600\nNew York\nUSA\n555-123-456\n' 'plokiploki\nplokiploki\n+011.555123456\napikey0001\n') result = self.invoke_with_exceptions(contact.create, args, input=inputs) self.assertEqual(re.sub(r'\[\d+\]', '[1234567890123456]', result.output.strip()), """\ Choose your contact type 0- individual 1- company 2- association 3- public body 4- reseller : 0 What is your first name: Peter What is your last name: Parker What is your email address: green.goblin@spiderman.org What is your street address: Central Park What is your zipcode: 2600 Which city: New York Which country: USA What is your telephone number: 555-123-456 Please enter your password [1234567890123456]: \ \nRepeat for confirmation: \ \nphone: string '555-123-456' does not match '^\\+\\d{1,3}\\.\\d+$' What is your telephone number: +011.555123456 planet: Pluto not in list Sun, Mercury, Venus, Earth, Mars, Jupiter, Saturn, \ Uranus, Neptune""") self.assertEqual(result.exit_code, 0) def test_create_ok(self): args = [] inputs = ('0\nPeter\nParker\npeter.parker@spiderman.org\n' 'Central Park\n2600\nNew York\nUSA\n+011.555123456\n' 'plokiploki\nplokiploki\napikey0001\n') result = self.invoke_with_exceptions(contact.create, args, input=inputs) self.assertEqual(re.sub(r'\[\d+\]', '[1234567890123456]', result.output.strip()), """\ Choose your contact type 0- individual 1- company 2- association 3- public body 4- reseller : 0 What is your first name: Peter What is your last name: Parker What is your email address: peter.parker@spiderman.org What is your street address: Central Park What is your zipcode: 2600 Which city: New York Which country: USA What is your telephone number: +011.555123456 Please enter your password [1234567890123456]: \ \nRepeat for confirmation: \ \nPlease activate you public api access from gandi website, and get the apikey. Your handle is PP0000-GANDI, and the password is the one you defined. What is your production apikey: apikey0001 You already have an apikey defined, if you want to use the newly created \ contact, use the env var : \ \nexport API_KEY=apikey0001""") self.assertEqual(result.exit_code, 0) def test_create_apikey_ok(self): args = [] inputs = ('0\nPeter\nParker\npeter.parker@spiderman.org\n' 'Central Park\n2600\nNew York\nUSA\n+011.555123456\n' 'plokiploki\nplokiploki\napikey0002\n') GandiModule._conffiles = {'global': {}} result = self.invoke_with_exceptions(contact.create, args, input=inputs) self.assertEqual(re.sub(r'\[\d+\]', '[1234567890123456]', result.output.strip()), """\ Choose your contact type 0- individual 1- company 2- association 3- public body 4- reseller : 0 What is your first name: Peter What is your last name: Parker What is your email address: peter.parker@spiderman.org What is your street address: Central Park What is your zipcode: 2600 Which city: New York Which country: USA What is your telephone number: +011.555123456 Please enter your password [1234567890123456]: \ \nRepeat for confirmation: \ \nPlease activate you public api access from gandi website, and get the apikey. Your handle is PP0000-GANDI, and the password is the one you defined. What is your production apikey: apikey0002 Will save your apikey into the config file.""") self.assertEqual(result.exit_code, 0) self.assertTrue('api' in GandiModule._conffiles['global']) api_key = GandiModule._conffiles['global']['api'].get('key') self.assertEqual(api_key,'apikey0002') gandi.cli-1.2/gandi/cli/tests/commands/test_disk.py0000644000175000017500000005700413227142754023236 0ustar sayounsayoun00000000000000import re from click.exceptions import ClickException from .base import CommandTestCase from gandi.cli.commands import disk from gandi.cli.core.utils.size import disk_check_size from gandi.cli.core.base import GandiContextHelper class DiskTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(disk.list, []) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 ---------- name : sys_server01 state : created size : 3072 ---------- name : data state : created size : 3072 ---------- name : snaptest state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_vm(self): result = self.invoke_with_exceptions(disk.list, ['--vm']) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 vm : vm1426759833 ---------- name : sys_server01 state : created size : 3072 vm : server01 ---------- name : data state : created size : 3072 vm : server01 ---------- name : snaptest state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_id(self): result = self.invoke_with_exceptions(disk.list, ['--id']) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 id : 4969232 ---------- name : sys_server01 state : created size : 3072 id : 4969249 ---------- name : data state : created size : 3072 id : 4970079 ---------- name : snaptest state : created size : 3072 id : 663497 ---------- name : newdisk state : created size : 3072 id : 4969233 """) self.assertEqual(result.exit_code, 0) def test_list_type(self): result = self.invoke_with_exceptions(disk.list, ['--type']) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 type : data ---------- name : sys_server01 state : created size : 3072 type : data ---------- name : data state : created size : 3072 type : data ---------- name : snaptest state : created size : 3072 type : snapshot ---------- name : newdisk state : created size : 3072 type : data """) self.assertEqual(result.exit_code, 0) def test_list_only_data(self): result = self.invoke_with_exceptions(disk.list, ['--only-data']) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 ---------- name : sys_server01 state : created size : 3072 ---------- name : data state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_only_snapshot(self): result = self.invoke_with_exceptions(disk.list, ['--only-snapshot']) self.assertEqual(result.output, """name : snaptest state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_snapshotprofile(self): result = self.invoke_with_exceptions(disk.list, ['--snapshotprofile']) self.assertEqual(result.output, """name : sys_1426759833 state : created size : 3072 ---------- name : sys_server01 state : created size : 3072 ---------- name : data state : created size : 3072 profile : minimal ---------- name : snaptest state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_filter_datacenter(self): args = ['--datacenter', 'LU-BI1'] result = self.invoke_with_exceptions(disk.list, args) self.assertEqual(result.output, """\ name : sys_1426759833 state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_attached(self): result = self.invoke_with_exceptions(disk.list, ['--attached']) self.assertEqual(result.output, """\ name : sys_1426759833 state : created size : 3072 ---------- name : sys_server01 state : created size : 3072 ---------- name : data state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_detached(self): result = self.invoke_with_exceptions(disk.list, ['--detached']) self.assertEqual(result.output, """\ name : snaptest state : created size : 3072 ---------- name : newdisk state : created size : 3072 """) self.assertEqual(result.exit_code, 0) def test_list_attached_detached_ko(self): args = ['--detached', '--attached'] result = self.invoke_with_exceptions(disk.list, args) self.assertEqual(result.output, """\ Usage: list [OPTIONS] Error: You cannot use both --attached and --detached. """) self.assertEqual(result.exit_code, 2) def test_info(self): result = self.invoke_with_exceptions(disk.info, ['sys_server01']) self.assertEqual(result.output, """name : sys_server01 state : created size : 3072 type : data id : 4969249 kernel : 3.12-x86_64 (hvm) cmdline : root=/dev/sda ro nosep console=ttyS0 datacenter: FR-SD2 vm : server01 """) self.assertEqual(result.exit_code, 0) def test_info_multiple(self): args = ['sys_server01', 'data'] result = self.invoke_with_exceptions(disk.info, args) self.assertEqual(result.output, """name : data state : created size : 3072 type : data id : 4970079 datacenter: FR-SD2 vm : server01 ---------- name : sys_server01 state : created size : 3072 type : data id : 4969249 kernel : 3.12-x86_64 (hvm) cmdline : root=/dev/sda ro nosep console=ttyS0 datacenter: FR-SD2 vm : server01 """) self.assertEqual(result.exit_code, 0) def test_check_size(self): result = disk_check_size(None, None, 2048) self.assertEqual(result, 2048) self.assertRaises(ClickException, disk_check_size, None, None, 2040) def test_detach(self): result = self.invoke_with_exceptions(disk.detach, ['data']) self.assertEqual(result.output.strip(), "Are you sure you want to detach data? [y/N]:") self.assertEqual(result.exit_code, 0) def test_detach_forced(self): result = self.invoke_with_exceptions(disk.detach, ['-f', 'data']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The disk is still attached to the vm 152967. Will detach it. Detaching your disk(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_detach_background(self): args = ['data', '--bg', '-f'] result = self.invoke_with_exceptions(disk.detach, args) self.assertEqual(result.output, """\ The disk is still attached to the vm 152967. Will detach it. [{'id': 200, 'step': 'WAIT'}] """) def test_attach(self): args = ['snaptest', 'server01'] result = self.invoke_with_exceptions(disk.attach, args) self.assertEqual(result.output.strip(), """\ Are you sure you want to attach disk 'snaptest' to vm 'server01'? [y/N]:\ """) self.assertEqual(result.exit_code, 0) def test_attach_forced(self): args = ['snaptest', 'server01', '-f'] result = self.invoke_with_exceptions(disk.attach, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Attaching your disk(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_attach_must_detach(self): args = ['data', 'vm1426759833'] result = self.invoke_with_exceptions(disk.attach, args, input='y\n') self.assertEqual(result.output.strip(), """\ Are you sure you want to attach disk 'data' to vm 'vm1426759833'? [y/N]: y\ \nThis disk is still attached Are you sure you want to detach data? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_attach_must_detach_forced(self): args = ['data', 'vm1426759833', '-f'] result = self.invoke_with_exceptions(disk.attach, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The disk is still attached to the vm 152967. Will detach it. Detaching your disk. \rProgress: [###] 100.00% 00:00:00 \ \nAttaching your disk(s). \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_attach_forced_background(self): args = ['snaptest', 'server01', '-f', '--bg'] result = self.invoke_with_exceptions(disk.attach, args) self.assertEqual(result.output.strip(), "{'id': 200, 'step': 'WAIT'}") self.assertEqual(result.exit_code, 0) def test_update_name(self): args = ['data', '--name', 'data2'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_kernel(self): args = ['data', '--kernel', '3.12-x86_64 (hvm)'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_kernel_unavailable(self): args = ['data', '--kernel', '3.12-x86_64'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(result.output, """\ Usage: update [OPTIONS] RESOURCE Error: Kernel 3.12-x86_64 is not available for disk data """) self.assertEqual(result.exit_code, 2) def test_update_cmdline(self): args = ['data', '--cmdline', 'root=/dev/xvda1 loglevel=4 console=hvc0 nosep ro'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_snapshotprofile_ko(self): args = ['data', '--snapshotprofile', '7'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(result.exit_code, 2) def test_update_snapshotprofile(self): args = ['data', '--snapshotprofile', '2'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_snapshotprofile_delete(self): args = ['data', '--delete-snapshotprofile'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_snapshotprofile_conflict(self): args = ['data', '--delete-snapshotprofile', '--snapshotprofile', '2'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(result.exit_code, 2) def test_update_size(self): args = ['data', '--size', '5G'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) self.assertEqual(self.api_calls['hosting.disk.update'][0][1], {'size': 5120}) def test_update_size_prefix(self): args = ['data', '--size', '+3G'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) self.assertEqual(self.api_calls['hosting.disk.update'][0][1], {'size': 6144}) def test_update_background(self): args = ['data', '--name', 'data2', '--bg'] result = self.invoke_with_exceptions(disk.update, args) self.assertEqual(result.output.strip(), "{'id': 200, 'step': 'WAIT'}") self.assertEqual(result.exit_code, 0) def test_delete(self): result = self.invoke_with_exceptions(disk.delete, ['data']) self.assertEqual(result.output.strip(), "Are you sure you want to delete disk 'data'? [y/N]:") self.assertEqual(result.exit_code, 0) def test_delete_multiple(self): result = self.invoke_with_exceptions(disk.delete, ['data', 'snaptest']) self.assertEqual(result.output.strip(), """\ Are you sure you want to delete disk 'data, snaptest'? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_delete_attached(self): result = self.invoke_with_exceptions(disk.delete, ['data', '-f']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ The disk is still attached to the vm 152967. Will detach it. Detaching your disk(s). \rProgress: [###] 100.00% 00:00:00 \ \nDeleting your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_force(self): result = self.invoke_with_exceptions(disk.delete, ['snaptest', '-f']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Deleting your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background(self): args = ['snaptest', '-f', '--bg'] result = self.invoke_with_exceptions(disk.delete, args) self.assertEqual(result.output.strip(), """\ id : 200 step : WAIT""") self.assertEqual(result.exit_code, 0) def test_rollback(self): args = ['snaptest'] result = self.invoke_with_exceptions(disk.rollback, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Disk rollback in progress. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_rollback_background(self): args = ['snaptest', '--bg'] result = self.invoke_with_exceptions(disk.rollback, args) self.assertEqual(result.output.strip(), "{'id': 200, 'step': 'WAIT'}") self.assertEqual(result.exit_code, 0) def test_migrate(self): args = ['snaptest'] result = self.invoke_with_exceptions(disk.migrate, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to migrate disk snaptest ? [y/N]: y\ \n* Starting the migration of disk snaptest from datacenter FR-SD2 to FR-SD3 Disk migration in progress. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_migrate_background(self): args = ['snaptest', '--bg', '-f'] result = self.invoke_with_exceptions(disk.migrate, args) self.assertEqual(result.output.strip(), """\ * Starting the migration of disk snaptest from datacenter FR-SD2 to FR-SD3 id : 200 step : WAIT""") self.assertEqual(result.exit_code, 0) def test_migrate_not_available(self): args = ['newdisk'] result = self.invoke_with_exceptions(disk.migrate, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ No datacenter is available for migration""") self.assertEqual(result.exit_code, 0) def test_migrate_noconfirm(self): args = ['snaptest'] result = self.invoke_with_exceptions(disk.migrate, args, input='\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to migrate disk snaptest ? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_migrate_attached(self): args = ['data'] result = self.invoke_with_exceptions(disk.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Cannot start the migration: disk data is attached. \ Please detach the disk before starting the migration.""") self.assertEqual(result.exit_code, 0) def test_snapshot(self): args = ['snaptest'] result = self.invoke_with_exceptions(disk.snapshot, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create_from'][0][0] self.assertTrue(params['name'].startswith('snp')) def test_snapshot_background(self): args = ['snaptest', '--bg'] result = self.invoke_with_exceptions(disk.snapshot, args) self.assertEqual(result.output.strip(), "{'id': 200, 'step': 'WAIT'}") self.assertEqual(result.exit_code, 0) def test_snapshot_name(self): args = ['snaptest', '--name', 'snappy'] result = self.invoke_with_exceptions(disk.snapshot, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create_from'][0][0] self.assertEqual(params['name'], 'snappy') def test_create_default_ok(self): args = [] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create'][0][0] self.assertEqual(params['type'], 'data') self.assertEqual(params['size'], 3072) self.assertTrue(params['name'].startswith('vdi')) def test_create_params(self): args = ['--name', 'newdisk', '--size', '5G', '--datacenter', 'FR-SD3', '--snapshotprofile', '3'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create'][0][0] self.assertEqual(params['datacenter_id'], 4) self.assertEqual(params['size'], 5120) self.assertEqual(params['name'], 'newdisk') self.assertEqual(params['snapshot_profile'], 3) def test_create_datacenter_closed(self): args = ['--name', 'newdisk', '--size', '5G', '--datacenter', 'US-BA1', '--snapshotprofile', '3'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_create_datacenter_limited(self): args = ['--name', 'newdisk', '--size', '5G', '--datacenter', 'FR-SD2', '--snapshotprofile', '3'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider using \ another datacenter. Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['size'], 5120) self.assertEqual(params['name'], 'newdisk') self.assertEqual(params['snapshot_profile'], 3) def test_create_params_snapshot_ko(self): args = ['--name', 'newdisk', '--size', '5G', '--datacenter', 'FR', '--snapshotprofile', '7'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(result.exit_code, 2) def test_create_default_background(self): args = ['--bg'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(result.output.strip(), """\ id : 200 step : WAIT""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create'][0][0] self.assertEqual(params['type'], 'data') self.assertEqual(params['size'], 3072) self.assertTrue(params['name'].startswith('vdi')) def test_create_vm(self): args = ['--vm', 'server01'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ VM server01 datacenter will be used instead of FR-SD5. Creating your disk. \rProgress: [###] 100.00% 00:00:00 \ \nAttaching your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.disk.create'][0][0] self.assertEqual(params['type'], 'data') self.assertEqual(params['size'], 3072) self.assertEqual(params['datacenter_id'], 1) self.assertTrue(params['name'].startswith('vdi')) def test_create_source(self): args = ['--source', 'sys_server01'] result = self.invoke_with_exceptions(disk.create, args, obj=GandiContextHelper()) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your disk. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params, disk_id = self.api_calls['hosting.disk.create_from'][0] self.assertEqual(params['type'], 'data') self.assertTrue(params['name'].startswith('vdi')) self.assertEqual(disk_id, 4969249) gandi.cli-1.2/gandi/cli/tests/commands/__init__.py0000644000175000017500000000000012501555263022761 0ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/commands/test_dns.py0000644000175000017500000005755713227142762023104 0ustar sayounsayoun00000000000000from ..compat import mock, ReasonableBytesIO from .base import CommandTestCase from gandi.cli.commands import dns # disable SSL requests warning for tests import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() RESPONSES = { 'https://dns.api.gandi.net/api/v5/domains': { 'status': 200, 'headers': 'application/json', 'body': [{'domain_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com', # noqa 'domain_records_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records', # noqa 'fqdn': 'iheartcli.com'}, {'domain_href': 'https://dns.api.gandi.net/api/v5/domains/cli.sexy', # noqa 'domain_records_href': 'https://dns.api.gandi.net/api/v5/domains/cli.sexy/records', # noqa 'fqdn': 'cli.sexy'}], }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com': { 'status': 200, 'headers': 'application/json', 'body': {'domain_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com', # noqa 'domain_keys_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys', # noqa 'domain_records_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records', # noqa 'fqdn': 'iheartcli.com', 'zone_href': 'https://dns.api.gandi.net/api/v5/zones/397c514-e7cb-11e6-9429-00163e6dc886', # noqa 'zone_records_href': 'https://dns.api.gandi.net/api/v5/zones/397c514-e7cb-11e6-9429-00163e6dc886/records', # noqa 'zone_uuid': '397c514-e7cb-11e6-9429-00163e6dc886'} }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records?sort_by=rrset_name': { # noqa 'status': 200, 'headers': 'application/json', 'body': [{'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/%40/A', # noqa 'rrset_name': '@', 'rrset_ttl': 10800, 'rrset_type': 'A', 'rrset_values': ['217.70.184.38']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/%40/MX', # noqa 'rrset_name': '@', 'rrset_ttl': 10800, 'rrset_type': 'MX', 'rrset_values': ['50 fb.mail.gandi.net.', '10 spool.mail.gandi.net.']}, # noqa {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/blog/CNAME', # noqa 'rrset_name': 'blog', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['blogs.vip.gandi.net.']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/imap/CNAME', # noqa 'rrset_name': 'imap', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['access.mail.gandi.net.']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/pop/CNAME', # noqa 'rrset_name': 'pop', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['access.mail.gandi.net.']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/smtp/CNAME', # noqa 'rrset_name': 'smtp', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['relay.mail.gandi.net.']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/webmail/CNAME', # noqa 'rrset_name': 'webmail', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['webmail.gandi.net.']}, {'rrset_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/www/CNAME', # noqa 'rrset_name': 'www', 'rrset_ttl': 10800, 'rrset_type': 'CNAME', 'rrset_values': ['webredir.vip.gandi.net.']}], }, 'https://dns.api.gandi.net/api/v5/dns/rrtypes': { 'status': 200, 'headers': 'application/json', 'body': ['A', 'AAAA', 'CAA', 'CDS', 'CNAME', 'DNAME', 'DS', 'LOC', 'MX', 'NS', 'PTR', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT', 'WKS'], }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records': { 'status': 201, 'headers': 'application/json', 'body': {'message': 'DNS Record Created'}, }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/blog/CNAME': { # noqa 'status': 204, 'headers': 'application/json', 'body': {}, }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys': { 'status': 200, 'headers': {'content-type': 'application/json', 'location': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/3415833-2314-4a86-ba1c-c3c58608a168'}, # noqa 'body': [{'algorithm': 13, 'algorithm_name': 'ECDSAP256SHA256', 'deleted': False, 'ds': 'iheartcli.com. 3600 IN DS 5411 13 2 6153c39cfe4ff8673635490515e19f5336f5b7ee9c5ca4572fc44b24a0e794a', # noqa 'flags': 256, 'fqdn': 'iheartcli.com', 'key_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/3415833-2314-4a86-ba1c-c3c58608a168', # noqa 'status': 'active', 'uuid': '3415833-2314-4a86-ba1c-c3c58608a168'}, {'algorithm': 13, 'algorithm_name': 'ECDSAP256SHA256', 'deleted': False, 'ds': 'iheartcli.com. 3600 IN DS 43819 13 2 b4e6ed591f28f4a269b9adfaedec836ea0fe63a8f7f5097108297afa5492b70', # noqa 'flags': 256, 'fqdn': 'iheartcli.com', 'key_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/adaab60-bb17-40ed-a13e-88376fe28c86', # noqa 'status': 'active', 'uuid': 'adaab60-bb17-40ed-a13e-88376fe28c86'}], }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/3415833-2314-4a86-ba1c-c3c58608a168': { # noqa 'status': 200, 'headers': 'application/json', 'body': {'algorithm': 13, 'algorithm_name': 'ECDSAP256SHA256', 'deleted': False, 'ds': 'iheartcli.com. 3600 IN DS 5411 13 2 6153c39cfe4ff8673635490515e19f5336f5b7ee9c5ca4572fc44b24a0e794a', # noqa 'flags': 256, 'fqdn': 'iheartcli.com', 'public_key': 'Gnhra3gcNHUL0d05Ia6F/tgBzDD/Km6c2XFZA9RAOcjk/qg9aodc79MQtsTx4/CBlTmCSRIxlXWm1yMmV3LOlw==', # noqa 'fingerprint': '626168cae12c674f38958b324e10c7bb63ed74cc9d649bf04766a7c095c865787', # noqa 'key_href': 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/3415833-2314-4a86-ba1c-c3c58608a168', # noqa 'status': 'active', 'tag': 40658, 'uuid': '3415833-2314-4a86-ba1c-c3c58608a168'}, }, 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/keys/adaab60-bb17-40ed-a13e-88376fe28c86': { # noqa 'status': 204, 'headers': 'application/json', 'body': {}, }, } def _mock_requests(method, url, *args, **kwargs): # print(method, url, args, kwargs) content = RESPONSES[url]['body'] headers = RESPONSES[url]['headers'] if kwargs.get('headers', {}).get('Accept') == 'text/plain': content = """\ @ 10800 IN A 217.70.184.38 @ 10800 IN MX 10 spool.mail.gandi.net. @ 10800 IN MX 50 fb.mail.gandi.net. @ 10800 IN SOA ns1.gandi.net. hostmaster.gandi.net. 197539823 10800 3600 604800 10800 blog 10800 IN CNAME blogs.vip.gandi.net. imap 10800 IN CNAME access.mail.gandi.net. pop 10800 IN CNAME access.mail.gandi.net. smtp 10800 IN CNAME relay.mail.gandi.net. webmail 10800 IN CNAME webmail.gandi.net. www 10800 IN CNAME webredir.vip.gandi.net.""" # noqa content_hdr = kwargs.get('headers', {}).get('Content-Type') if method == 'PUT' and content_hdr == 'text/plain': content = {'message': 'DNS Record Created'} if method == 'PUT' and url == 'https://dns.api.gandi.net/api/v5/domains/iheartcli.com/records/blog/CNAME': # noqa content = {'message': 'DNS Record Created'} mock_resp = mock.Mock() mock_resp.status_code = 200 mock_resp.content = content mock_resp.headers = headers mock_resp.json = mock.Mock(return_value=content) return mock_resp class DnsTestCase(CommandTestCase): @mock.patch('gandi.cli.core.client.requests.request') def test_dns_domain_list(self, mock_request): mock_request.side_effect = _mock_requests result = self.invoke_with_exceptions(dns.domain_list, []) wanted = """\ iheartcli.com cli.sexy """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_list(self, mock_request): mock_request.side_effect = _mock_requests result = self.invoke_with_exceptions(dns.list, ['iheartcli.com']) wanted = """\ name : @ ttl : 10800 type : A values : 217.70.184.38 ---------- name : @ ttl : 10800 type : MX values : 50 fb.mail.gandi.net., 10 spool.mail.gandi.net. ---------- name : blog ttl : 10800 type : CNAME values : blogs.vip.gandi.net. ---------- name : imap ttl : 10800 type : CNAME values : access.mail.gandi.net. ---------- name : pop ttl : 10800 type : CNAME values : access.mail.gandi.net. ---------- name : smtp ttl : 10800 type : CNAME values : relay.mail.gandi.net. ---------- name : webmail ttl : 10800 type : CNAME values : webmail.gandi.net. ---------- name : www ttl : 10800 type : CNAME values : webredir.vip.gandi.net. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_list_filter(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', '--type', 'CNAME'] result = self.invoke_with_exceptions(dns.list, args) wanted = """\ ---------- name : blog ttl : 10800 type : CNAME values : blogs.vip.gandi.net. ---------- name : imap ttl : 10800 type : CNAME values : access.mail.gandi.net. ---------- name : pop ttl : 10800 type : CNAME values : access.mail.gandi.net. ---------- name : smtp ttl : 10800 type : CNAME values : relay.mail.gandi.net. ---------- name : webmail ttl : 10800 type : CNAME values : webmail.gandi.net. ---------- name : www ttl : 10800 type : CNAME values : webredir.vip.gandi.net. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_list_filter_args(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'CNAME'] result = self.invoke_with_exceptions(dns.list, args) wanted = """\ ---------- name : blog ttl : 10800 type : CNAME values : blogs.vip.gandi.net. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_list_unknown(self, mock_request): mock_request.side_effect = _mock_requests result = self.invoke_with_exceptions(dns.list, ['example.com']) wanted = """\ Sorry domain example.com does not exist Please use one of the following: iheartcli.com, cli.sexy """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_list_text(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', '--text'] result = self.invoke_with_exceptions(dns.list, args) wanted = """\ @ 10800 IN A 217.70.184.38 @ 10800 IN MX 10 spool.mail.gandi.net. @ 10800 IN MX 50 fb.mail.gandi.net. @ 10800 IN SOA ns1.gandi.net. hostmaster.gandi.net. 197539823 10800 3600 604800 10800 blog 10800 IN CNAME blogs.vip.gandi.net. imap 10800 IN CNAME access.mail.gandi.net. pop 10800 IN CNAME access.mail.gandi.net. smtp 10800 IN CNAME relay.mail.gandi.net. webmail 10800 IN CNAME webmail.gandi.net. www 10800 IN CNAME webredir.vip.gandi.net. """ # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_create(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'cname', 'blog.cli.sexy'] result = self.invoke_with_exceptions(dns.create, args) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_missing(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com'] result = self.invoke_with_exceptions(dns.update, args) wanted = """Cannot find parameters for zone content to update. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_unknown(self, mock_request): mock_request.side_effect = _mock_requests args = ['example.com'] result = self.invoke_with_exceptions(dns.update, args) wanted = """\ Sorry domain example.com does not exist Please use one of the following: iheartcli.com, cli.sexy """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_argument_ok(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', '--file', 'sandbox/example.txt'] content = """\ blog 10800 IN CNAME blogs.vip.gandi.net. """ result = self.isolated_invoke_with_exceptions(dns.update, args, temp_content=content) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_parameters_ok(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'cname', 'blog.cli.sexy'] result = self.invoke_with_exceptions(dns.update, args) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_parameters_ko(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'cname'] result = self.invoke_with_exceptions(dns.update, args) wanted = """You must provide one or more value parameter. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_update_pipe_ok(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com'] content = b"""\ blog 10800 IN CNAME blogs.vip.gandi.net. """ result = self.invoke_with_exceptions(dns.update, args, input=ReasonableBytesIO(content)) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_create_multiple(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'cname', 'blog.cli.sexy', 'glop.cli.sexy'] result = self.invoke_with_exceptions(dns.create, args) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_create_type_case(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'CNAME', 'blog.cli.sexy'] result = self.invoke_with_exceptions(dns.create, args) wanted = """DNS Record Created """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_create_unknown(self, mock_request): mock_request.side_effect = _mock_requests args = ['example.com', 'blog', 'CNAME', 'blog.cli.sexy'] result = self.invoke_with_exceptions(dns.create, args) wanted = """\ Sorry domain example.com does not exist Please use one of the following: iheartcli.com, cli.sexy """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_delete(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'CNAME', '-f'] result = self.invoke_with_exceptions(dns.delete, args) wanted = """Delete successful. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_delete_unknown(self, mock_request): mock_request.side_effect = _mock_requests args = ['example.com', 'blog', 'CNAME', '-f'] result = self.invoke_with_exceptions(dns.delete, args) wanted = """\ Sorry domain example.com does not exist Please use one of the following: iheartcli.com, cli.sexy """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_delete_all(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com'] result = self.invoke_with_exceptions(dns.delete, args, input='\n') wanted = """\ Are you sure to delete all records for domain iheartcli.com ? [y/N]: \n""" self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_delete_name(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog'] result = self.invoke_with_exceptions(dns.delete, args, input='\n') wanted = """\ Are you sure to delete all 'blog' name records for domain iheartcli.com ? [y/N]: \n""" # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_delete_prompt(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'blog', 'CNAME'] result = self.invoke_with_exceptions(dns.delete, args, input='\n') wanted = """\ Are you sure to delete all 'blog' records of type CNAME for domain iheartcli.com ? [y/N]: \n""" # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_list(self, mock_request): mock_request.side_effect = _mock_requests result = self.invoke_with_exceptions(dns.keys_list, ['iheartcli.com']) wanted = """\ uuid : 3415833-2314-4a86-ba1c-c3c58608a168 algorithm : 13 algorithm_name : ECDSAP256SHA256 ds : iheartcli.com. 3600 IN DS 5411 13 2 6153c39cfe4ff8673635490515e19f5336f5b7ee9c5ca4572fc44b24a0e794a flags : 256 status : active ---------- uuid : adaab60-bb17-40ed-a13e-88376fe28c86 algorithm : 13 algorithm_name : ECDSAP256SHA256 ds : iheartcli.com. 3600 IN DS 43819 13 2 b4e6ed591f28f4a269b9adfaedec836ea0fe63a8f7f5097108297afa5492b70 flags : 256 status : active """ # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_info(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', '3415833-2314-4a86-ba1c-c3c58608a168'] result = self.invoke_with_exceptions(dns.keys_info, args) wanted = """\ uuid : 3415833-2314-4a86-ba1c-c3c58608a168 algorithm : 13 algorithm_name : ECDSAP256SHA256 ds : iheartcli.com. 3600 IN DS 5411 13 2 6153c39cfe4ff8673635490515e19f5336f5b7ee9c5ca4572fc44b24a0e794a fingerprint : 626168cae12c674f38958b324e10c7bb63ed74cc9d649bf04766a7c095c865787 public_key : Gnhra3gcNHUL0d05Ia6F/tgBzDD/Km6c2XFZA9RAOcjk/qg9aodc79MQtsTx4/CBlTmCSRIxlXWm1yMmV3LOlw== flags : 256 tag : 40658 status : active """ # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_create(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', '256'] result = self.invoke_with_exceptions(dns.keys_create, args) wanted = """\ uuid : 3415833-2314-4a86-ba1c-c3c58608a168 algorithm : 13 algorithm_name : ECDSAP256SHA256 ds : iheartcli.com. 3600 IN DS 5411 13 2 6153c39cfe4ff8673635490515e19f5336f5b7ee9c5ca4572fc44b24a0e794a fingerprint : 626168cae12c674f38958b324e10c7bb63ed74cc9d649bf04766a7c095c865787 public_key : Gnhra3gcNHUL0d05Ia6F/tgBzDD/Km6c2XFZA9RAOcjk/qg9aodc79MQtsTx4/CBlTmCSRIxlXWm1yMmV3LOlw== flags : 256 tag : 40658 status : active """ # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_delete_ok(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'adaab60-bb17-40ed-a13e-88376fe28c86', '-f'] result = self.invoke_with_exceptions(dns.keys_delete, args) wanted = """Delete successful. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_delete_prompt(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'adaab60-bb17-40ed-a13e-88376fe28c86'] result = self.invoke_with_exceptions(dns.keys_delete, args, input='\n') wanted = """\ Are you sure you want to delete key adaab60-bb17-40ed-a13e-88376fe28c86 on \ domain iheartcli.com? [y/N]: \n""" self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.client.requests.request') def test_dns_keys_recover(self, mock_request): mock_request.side_effect = _mock_requests args = ['iheartcli.com', 'adaab60-bb17-40ed-a13e-88376fe28c86'] result = self.invoke_with_exceptions(dns.keys_recover, args) wanted = """Recover successful. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_vhost.py0000644000175000017500000001233512715030026023432 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- import re from .base import CommandTestCase from gandi.cli.commands import vhost class VhostTestCase(CommandTestCase): def test_list(self): args = ['--id', '--names'] result = self.invoke_with_exceptions(vhost.list, args) self.assertEqual(result.output, """\ name : aa3e0e26f8.url-de-test.ws state : running date_creation : 20130903T22:11:54 paas_id : 126276 paas_name : paas_owncloud ---------- name : cloud.cat.lol state : running date_creation : 20130903T22:24:06 paas_id : 126276 paas_name : paas_owncloud ---------- name : 187832c2b34.testurl.ws state : running date_creation : 20141025T15:50:54 paas_id : 163744 paas_name : paas_cozycloud ---------- name : cloud.iheartcli.com state : running date_creation : 20141025T15:50:54 paas_id : 163744 paas_name : paas_cozycloud ---------- name : cli.sexy state : running date_creation : 20150728T17:50:56 paas_id : 163744 paas_name : paas_cozycloud """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['cloud.cat.lol', 'cloud.iheartcli.com', '--id'] result = self.invoke_with_exceptions(vhost.info, args) self.assertEqual(result.output, """\ name : cloud.cat.lol state : running date_creation : 20130903T22:24:06 ssl : disabled paas_id : 126276 paas_name : paas_owncloud ---------- name : cloud.iheartcli.com state : running date_creation : 20141025T15:50:54 ssl : disabled paas_id : 163744 paas_name : paas_cozycloud """) self.assertEqual(result.exit_code, 0) def test_create(self): args = ['pouet.lol.cat', '--paas', 'paas_owncloud'] result = self.invoke_with_exceptions(vhost.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating a new vhost. \rProgress: [###] 100.00% 00:00:00 \ \nYour vhost pouet.lol.cat has been created.""") self.assertEqual(result.exit_code, 0) def test_create_ssl(self): args = ['pouet.lol.cat', '--paas', 'paas_owncloud', '--ssl'] result = self.invoke_with_exceptions(vhost.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please give the private key for certificate id 710 (CN: lol.cat)""") self.assertEqual(result.exit_code, 0) def test_create_background(self): args = ['pouet.lol.cat', '--paas', 'paas_owncloud', '--bg'] result = self.invoke_with_exceptions(vhost.create, args) self.assertEqual(result.output.strip(), """\ {'id': 200, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) def test_update_ssl_ko(self): args = ['pouet.lol.cat', '--ssl'] result = self.invoke_with_exceptions(vhost.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please give the private key for certificate id 710 (CN: lol.cat)""") self.assertEqual(result.exit_code, 0) def test_update_ssl_ok(self): args = ['unknown.lol.cat', '--ssl'] result = self.invoke_with_exceptions(vhost.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ There is no certificate for unknown.lol.cat. Create the certificate with (for exemple) : $ gandi certificate create --cn unknown.lol.cat --type std \ \nOr relaunch the current command with --poll-cert option""") self.assertEqual(result.exit_code, 0) def test_delete_prompt_ok(self): args = ['pouet.lol.cat'] result = self.invoke_with_exceptions(vhost.delete, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete vhost pouet.lol.cat? [y/N]: y Deleting your vhost. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_prompt_ko(self): args = ['pouet.lol.cat'] result = self.invoke_with_exceptions(vhost.delete, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete vhost pouet.lol.cat? [y/N]: N""") self.assertEqual(result.exit_code, 0) def test_delete_force(self): args = ['pouet.lol.cat', '--force'] result = self.invoke_with_exceptions(vhost.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Deleting your vhost. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background(self): args = ['pouet.lol.cat', '--force', '--bg'] result = self.invoke_with_exceptions(vhost.delete, args) self.assertEqual(result.output.strip(), """\ name : rproxy_update paas_id : 1177220 date_creation: 20150728T17:50:56""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_mail.py0000644000175000017500000001277012766207173023233 0ustar sayounsayoun00000000000000import re from .base import CommandTestCase from gandi.cli.commands import mail from gandi.cli.core.base import GandiContextHelper class MailTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(mail.list, ['iheartcli.com']) self.assertEqual(result.output, """admin """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(mail.info, args) self.assertEqual(result.output, """login : admin aliases : fallback email : quota usage : 233 KiB / unlimited responder active: no responder text : """) self.assertEqual(result.exit_code, 0) def test_create(self): args = ['contact@iheartcli.com', '--quota', '2', '--fallback', 'admin@cli.sexy', '--alias', 'god@iheartcli.com'] result = self.invoke_with_exceptions(mail.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(result.output, """password: \ \nRepeat for confirmation: \ \nCreating your mailbox. Creating aliases. """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.mailbox.create'][0][2] self.assertEqual(params['password'], 'plokiploki') self.assertEqual(params['quota'], 2) self.assertEqual(params['fallback_email'], 'admin@cli.sexy') def test_create_with_password(self): args = ['contact2@iheartcli.com', '--quota', '2', '--fallback', 'admin@cli.sexy', '--alias', 'god@iheartcli.com', '--password', 'password_for_create'] result = self.invoke_with_exceptions(mail.create, args, obj=GandiContextHelper()) self.assertEqual(result.output, """Creating your mailbox. Creating aliases. """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.mailbox.create'][0][2] self.assertEqual(params['password'], 'password_for_create') self.assertEqual(params['quota'], 2) self.assertEqual(params['fallback_email'], 'admin@cli.sexy') def test_delete_force(self): args = ['admin@iheartcli.com', '--force'] result = self.invoke_with_exceptions(mail.delete, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(mail.delete, args, input='y\n') self.assertEqual(result.output, """\ Are you sure to delete the mailbox admin@iheartcli.com ? [y/N]: y """) self.assertEqual(result.exit_code, 0) def test_delete_force_refused(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(mail.delete, args, input='\n') self.assertEqual(result.output, """\ Are you sure to delete the mailbox admin@iheartcli.com ? [y/N]: \n""") self.assertEqual(result.exit_code, 0) def test_udpate(self): args = ['admin@iheartcli.com', '--quota', '2', '--fallback', 'admin@cli.sexy', '--alias-add', 'doge@iheartcli.com', '--alias-del', 'god@iheartcli.com', '--password'] result = self.invoke_with_exceptions(mail.update, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(result.output, """password: \ \nRepeat for confirmation: \ \nUpdating your mailbox. Updating aliases. """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.mailbox.update'][0][2] self.assertEqual(params['password'], 'plokiploki') self.assertEqual(params['quota'], 2) self.assertEqual(params['fallback_email'], 'admin@cli.sexy') def test_purge(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(mail.purge, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to purge mailbox admin@iheartcli.com ? [y/N]: y Purging in progress \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_purge_alias(self): args = ['admin@iheartcli.com', '--alias'] result = self.invoke_with_exceptions(mail.purge, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to purge all aliases for mailbox admin@iheartcli.com ? [y/N]: y\ """) self.assertEqual(result.exit_code, 0) def test_purge_alias_refused(self): args = ['admin@iheartcli.com', '--alias'] result = self.invoke_with_exceptions(mail.purge, args, input='\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to purge all aliases for mailbox admin@iheartcli.com ? [y/N]:\ """) self.assertEqual(result.exit_code, 0) def test_purge_refused(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(mail.purge, args, input='\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to purge mailbox admin@iheartcli.com ? [y/N]:\ """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_forward.py0000644000175000017500000000530212656121545023742 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from .base import CommandTestCase from gandi.cli.commands import forward class ForwardTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(forward.list, ['iheartcli.com']) self.assertEqual(result.output, """\ admin : admin@cli.sexy admin : grumpy@cat.lol contact : contact@cli.sexy """) self.assertEqual(result.exit_code, 0) def test_create(self): args = ['backup@iheartcli.com', '--destination', 'backup@cat.lol'] result = self.invoke_with_exceptions(forward.create, args) self.assertEqual(result.output, """\ Creating mail forward backup@iheartcli.com """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.forward.create'][0][2] self.assertEqual(params['destinations'], ['backup@cat.lol']) def test_delete_force(self): args = ['admin@iheartcli.com', '--force'] result = self.invoke_with_exceptions(forward.delete, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(forward.delete, args, input='y\n') self.assertEqual(result.output, """\ Are you sure to delete the domain mail forward admin@iheartcli.com ? [y/N]: y """) self.assertEqual(result.exit_code, 0) def test_delete_force_refused(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(forward.delete, args, input='\n') self.assertEqual(result.output, """\ Are you sure to delete the domain mail forward admin@iheartcli.com ? [y/N]: \ \n""") self.assertEqual(result.exit_code, 0) def test_update(self): args = ['admin@iheartcli.com', '--dest-add', 'doge@iheartcli.com', '--dest-del', 'grumpy@cat.lol'] result = self.invoke_with_exceptions(forward.update, args) self.assertEqual(result.output, """\ Updating mail forward admin@iheartcli.com """) self.assertEqual(result.exit_code, 0) params = self.api_calls['domain.forward.update'][0][2] self.assertEqual(params['destinations'], ['admin@cli.sexy', 'doge@iheartcli.com']) def test_update_no_param(self): args = ['admin@iheartcli.com'] result = self.invoke_with_exceptions(forward.update, args) self.assertEqual(result.output, """\ Nothing to update: you must provide destinations to update, use \ --dest-add/-a or -dest-del/-d parameters. """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_paas.py0000644000175000017500000005672713227414171023236 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from functools import partial import re from ..compat import mock, StringIO, ConfigParser from .base import CommandTestCase from gandi.cli.commands import paas from gandi.cli.core.base import GandiContextHelper def _mock_output(git_content, command, *args, **kwargs): buf = StringIO(git_content) config = ConfigParser.ConfigParser() try: config.read_file(buf) except: config.readfp(buf) if command == 'git config --local --get remote.gandi.url': try: return config.get('remote "gandi"', 'url') except: return '' if command == 'git config --local --get remote.origin.url': try: return config.get('remote "origin"', 'url') except: return '' if command == 'git config --local --get remote.$(git config --local --get branch.stable.remote).url': # noqa try: return config.get('remote "production"', 'url') except: return '' if command == 'git config --local --get remote.$(git config --local --get branch.master.remote).url': # noqa try: return config.get('remote "origin"', 'url') except: return '' return '' class PaasTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(paas.list, []) self.assertEqual(result.output, """\ name : paas_owncloud state : halted vhost : aa3e0e26f8.url-de-test.ws vhost : cloud.cat.lol ---------- name : paas_cozycloud state : running vhost : 187832c2b34.testurl.ws vhost : cloud.iheartcli.com vhost : cli.sexy """) self.assertEqual(result.exit_code, 0) def test_list_id(self): result = self.invoke_with_exceptions(paas.list, ['--id']) self.assertEqual(result.output, """\ name : paas_owncloud state : halted id : 126276 vhost : aa3e0e26f8.url-de-test.ws vhost : cloud.cat.lol ---------- name : paas_cozycloud state : running id : 163744 vhost : 187832c2b34.testurl.ws vhost : cloud.iheartcli.com vhost : cli.sexy """) self.assertEqual(result.exit_code, 0) def test_list_type(self): result = self.invoke_with_exceptions(paas.list, ['--type']) self.assertEqual(result.output, """\ name : paas_owncloud state : halted type : phpmysql vhost : aa3e0e26f8.url-de-test.ws vhost : cloud.cat.lol ---------- name : paas_cozycloud state : running type : nodejsmongodb vhost : 187832c2b34.testurl.ws vhost : cloud.iheartcli.com vhost : cli.sexy """) self.assertEqual(result.exit_code, 0) def test_list_filter_state(self): result = self.invoke_with_exceptions(paas.list, ['--state', 'halted']) self.assertEqual(result.output, """\ name : paas_owncloud state : halted vhost : aa3e0e26f8.url-de-test.ws vhost : cloud.cat.lol """) self.assertEqual(result.exit_code, 0) def test_info(self): result = self.invoke_with_exceptions(paas.info, ['paas_cozycloud']) self.assertEqual(result.output, """\ name : paas_cozycloud type : nodejsmongodb size : s console : 185290@console.dc2.gpaas.net git_server : git.dc2.gpaas.net sftp_server: sftp.dc2.gpaas.net vhost : 187832c2b34.testurl.ws vhost : cloud.iheartcli.com vhost : cli.sexy datacenter : LU-BI1 quota used : 0.5% snapshot : """) self.assertEqual(result.exit_code, 0) def test_info_stat(self): args = ['paas_cozycloud', '--stat'] result = self.invoke_with_exceptions(paas.info, args) self.assertEqual(result.output, """\ name : paas_cozycloud type : nodejsmongodb size : s console : 185290@console.dc2.gpaas.net git_server : git.dc2.gpaas.net sftp_server: sftp.dc2.gpaas.net vhost : 187832c2b34.testurl.ws vhost : cloud.iheartcli.com vhost : cli.sexy datacenter : LU-BI1 quota used : 0.5% snapshot : cache : \thit : 0.0% \tmiss : 100.0% \tnot : 0.0% \tpass : 0.0% """) self.assertEqual(result.exit_code, 0) def test_types(self): result = self.invoke_with_exceptions(paas.types, []) self.assertEqual(result.output, """\ phpmysql phppgsql nodejspgsql nodejsmongodb nodejsmysql phpmongodb pythonmysql pythonpgsql pythonmongodb rubymysql rubypgsql rubymongodb """) self.assertEqual(result.exit_code, 0) def test_console(self): result = self.invoke_with_exceptions(paas.console, ['paas_cozycloud']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Use ~. ssh escape key to exit. Activation of the console on your PaaS instance \rProgress: [###] 100.00% 00:00:00 \n\ ssh 185290@console.dc2.gpaas.net""") self.assertEqual(result.exit_code, 0) def test_clone_missing(self): result = self.invoke_with_exceptions(paas.clone, []) self.assertEqual(result.output, """\ Usage: clone [OPTIONS] NAME Error: Missing argument "name". """) self.assertEqual(result.exit_code, 2) def test_clone(self): with mock.patch('gandi.cli.modules.vhost.os.chdir', create=True) as mock_chdir: mock_chdir.return_value = mock.MagicMock() result = self.invoke_with_exceptions(paas.clone, ['cli.sexy']) self.assertEqual(result.output, """\ git clone ssh+git://185290@git.dc2.gpaas.net/default.git cli.sexy \ --origin gandi Use `git push gandi master` to push your code to the instance. Then `$ gandi deploy` to build and deploy your application. """) self.assertEqual(result.exit_code, 0) def test_clone_directory(self): with mock.patch('gandi.cli.modules.vhost.os.chdir', create=True) as mock_chdir: mock_chdir.return_value = mock.MagicMock() args = ['cli.sexy', '--directory', 'project'] result = self.invoke_with_exceptions(paas.clone, args) self.assertEqual(result.output, """\ git clone ssh+git://185290@git.dc2.gpaas.net/default.git project --origin gandi Use `git push gandi master` to push your code to the instance. Then `$ gandi deploy` to build and deploy your application. """) self.assertEqual(result.exit_code, 0) def test_clone_vhost(self): with mock.patch('gandi.cli.modules.vhost.os.chdir', create=True) as mock_chdir: mock_chdir.return_value = mock.MagicMock() args = ['paas_cozycloud', '--vhost', 'cli.sexy'] result = self.invoke_with_exceptions(paas.clone, args) self.assertEqual(result.output, """\ git clone ssh+git://185290@git.dc2.gpaas.net/cli.sexy.git cli.sexy \ --origin gandi Use `git push gandi master` to push your code to the instance. Then `$ gandi deploy` to build and deploy your application. """) self.assertEqual(result.exit_code, 0) def test_attach(self): result = self.invoke_with_exceptions(paas.attach, ['paas_cozycloud']) self.assertEqual(result.output, """\ git remote add gandi ssh+git://185290@git.dc2.gpaas.net/default.git Added remote `gandi` to your local git repository. Use `git push gandi master` to push your code to the instance. Then `$ gandi deploy` to build and deploy your application. """) self.assertEqual(result.exit_code, 0) def test_attach_remote(self): args = ['paas_cozycloud', '--remote', 'production'] result = self.invoke_with_exceptions(paas.attach, args) self.assertEqual(result.output, """\ git remote add production ssh+git://185290@git.dc2.gpaas.net/default.git Added remote `production` to your local git repository. Use `git push production master` to push your code to the instance. Then `$ gandi deploy` to build and deploy your application. """) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.base.GandiModule.exec_output') def test_deploy_invalid_remote_empty(self, mock_exec_output): args = [] git_content = """ [blabla] dummy=dududududud """ mock_exec_output.side_effect = partial(_mock_output, git_content) result = self.invoke_with_exceptions(paas.deploy, args) self.assertEqual(result.output, """\ Error: Could not find git remote to extract deploy url from. This usually happens when: - the current directory has no Simple Hosting git remote attached, in this case, please see $ gandi paas attach --help - the local branch being deployed hasn't been pushed \ to the remote repository yet, in this case, please try $ git push master Otherwise, it's recommended to use the --remote and/or --branch options: $ gandi deploy --remote [--branch ] """) self.assertEqual(result.exit_code, 2) @mock.patch('gandi.cli.core.base.GandiModule.exec_output') def test_deploy_invalid_remote_content(self, mock_exec_output): args = [] self.maxDiff = None git_content = """ [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = https://github.com/Gandi/gandi.cli.git [branch "master"] remote = origin merge = refs/heads/master """ mock_exec_output.side_effect = partial(_mock_output, git_content) result = self.invoke_with_exceptions(paas.deploy, args) self.assertEqual(result.output, """\ Error: https://github.com/Gandi/gandi.cli.git \ is not a valid Simple Hosting git remote. This usually happens when: - the current directory has no Simple Hosting git remote attached, in this case, please see $ gandi paas attach --help - the local branch being deployed hasn't been pushed \ to the remote repository yet, in this case, please try $ git push master Otherwise, it's recommended to use the --remote and/or --branch options: $ gandi deploy --remote [--branch ] """) self.assertEqual(result.exit_code, 2) @mock.patch('gandi.cli.core.base.GandiModule.exec_output') def test_deploy(self, mock_exec_output): args = [] git_content = """ [remote "gandi"] fetch = +refs/heads/*:refs/remotes/gandi/* url = ssh+git://185290@git.dc2.gpaas.net/default.git """ mock_exec_output.side_effect = partial(_mock_output, git_content) result = self.invoke_with_exceptions(paas.deploy, args) self.assertEqual(result.output, """\ ssh 185290@git.dc2.gpaas.net 'deploy default.git master' """) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.base.GandiModule.exec_output') def test_deploy_remote(self, mock_exec_output): args = ['--remote', 'origin'] git_content = """ [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = ssh+git://185290@git.dc2.gpaas.net/default.git [branch "master"] remote = origin merge = refs/heads/master """ mock_exec_output.side_effect = partial(_mock_output, git_content) result = self.invoke_with_exceptions(paas.deploy, args) self.assertEqual(result.output, """\ ssh 185290@git.dc2.gpaas.net 'deploy default.git master' """) self.assertEqual(result.exit_code, 0) @mock.patch('gandi.cli.core.base.GandiModule.exec_output') def test_deploy_guess_remote_with_branch(self, mock_exec_output): args = ['--branch', 'stable'] git_content = """ [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = https://github.com/Gandi/gandi.cli.git [remote "production"] fetch = +refs/heads/*:refs/remotes/production/* url = ssh+git://185290@git.dc2.gpaas.net/default.git [branch "master"] remote = origin merge = refs/heads/master [branch "stable"] remote = production merge = refs/heads/stable """ mock_exec_output.side_effect = partial(_mock_output, git_content) result = self.invoke_with_exceptions(paas.deploy, args) self.assertEqual(result.output, """\ ssh 185290@git.dc2.gpaas.net 'deploy default.git stable' """) self.assertEqual(result.exit_code, 0) def test_delete_unknown(self): result = self.invoke_with_exceptions(paas.delete, ['unknown_paas']) self.assertEqual(result.output, """\ Sorry PaaS instance unknown_paas does not exist Please use one of the following: ['paas_owncloud', 'paas_cozycloud', \ '126276', '163744', 'aa3e0e26f8.url-de-test.ws', 'cloud.cat.lol', \ '187832c2b34.testurl.ws', 'cloud.iheartcli.com', 'cli.sexy'] """) self.assertEqual(result.exit_code, 0) def test_delete(self): result = self.invoke_with_exceptions(paas.delete, ['paas_owncloud']) self.assertEqual(result.output.strip(), """\ Are you sure to delete PaaS instance 'paas_owncloud'? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_delete_ko(self): args = ['paas_owncloud'] result = self.invoke_with_exceptions(paas.delete, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete PaaS instance 'paas_owncloud'? [y/N]: N\ """) self.assertEqual(result.exit_code, 0) def test_delete_ok(self): args = ['paas_owncloud'] result = self.invoke_with_exceptions(paas.delete, args, input='y\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to delete PaaS instance 'paas_owncloud'? [y/N]: y Deleting your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background(self): args = ['paas_owncloud', '--force', '--background'] result = self.invoke_with_exceptions(paas.delete, args) self.assertEqual(result.output, """\ id : 200 step : WAIT """) self.assertEqual(result.exit_code, 0) def test_create_default(self): args = [] result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'paas\d+', 'paas', output), """\ password: \nRepeat for confirmation: \n\ Creating your PaaS instance. \rProgress: [###] 100.00% 00:00:00 \n\ Your PaaS instance paas has been created.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['paas.create'][0][0] self.assertEqual(params['datacenter_id'], 3) self.assertEqual(params['size'], 's') self.assertEqual(params['duration'], '1m') self.assertEqual(params['password'], 'ploki') self.assertTrue(params['name'].startswith('paas')) def test_create_size(self): args = ['--size', 's+'] result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'paas\d+', 'paas', output), """\ password: \nRepeat for confirmation: \n\ Creating your PaaS instance. \rProgress: [###] 100.00% 00:00:00 \n\ Your PaaS instance paas has been created.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['paas.create'][0][0] self.assertEqual(params['datacenter_id'], 3) self.assertEqual(params['size'], 's+') self.assertEqual(params['duration'], '1m') self.assertEqual(params['password'], 'ploki') self.assertTrue(params['name'].startswith('paas')) def test_create_name(self): args = ['--name', '123456'] result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n\ Creating your PaaS instance. \rProgress: [###] 100.00% 00:00:00 \n\ Your PaaS instance 123456 has been created.""") self.assertEqual(result.exit_code, 0) def test_create_name_vhost(self): self.maxDiff = None args = ['--name', '123456', '--vhosts', 'ploki.fr', '--ssl'] with mock.patch('gandi.cli.modules.vhost.os.chdir', create=True) as mock_chdir: mock_chdir.return_value = mock.MagicMock() result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n\ There is no certificate for ploki.fr. Create the certificate with (for exemple) : $ gandi certificate create --cn ploki.fr --type std \n\ Or relaunch the current command with --poll-cert option Creating your PaaS instance. \rProgress: [###] 100.00% 00:00:00 \n\ Your PaaS instance 123456 has been created. Creating a new vhost. \rProgress: [###] 100.00% 00:00:00 \n\ Your vhost ploki.fr has been created.""") self.assertEqual(result.exit_code, 0) def test_create_name_vhost_ssl(self): self.maxDiff = None args = ['--name', '123456', '--vhosts', 'inter.net', '--ssl'] with mock.patch('gandi.cli.modules.vhost.os.chdir', create=True) as mock_chdir: mock_chdir.return_value = mock.MagicMock() result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n\ Please give the private key for certificate id 706 (CN: inter.net)""") self.assertEqual(result.exit_code, 0) def test_create_datacenter_limited(self): args = ['--datacenter', 'FR-SD2'] result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'paas\d+', 'paas', output), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider using \ another datacenter. password: \nRepeat for confirmation: \n\ Creating your PaaS instance. \rProgress: [###] 100.00% 00:00:00 \n\ Your PaaS instance paas has been created.""") self.assertEqual(result.exit_code, 0) params = self.api_calls['paas.create'][0][0] self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['size'], 's') self.assertEqual(params['duration'], '1m') self.assertEqual(params['password'], 'ploki') self.assertTrue(params['name'].startswith('paas')) self.assertEqual(result.exit_code, 0) def test_create_datacenter_closed(self): args = ['--datacenter', 'US-BA1'] result = self.invoke_with_exceptions(paas.create, args, obj=GandiContextHelper(), input='ploki\nploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'paas\d+', 'paas', output), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_update(self): args = ['paas_owncloud'] result = self.invoke_with_exceptions(paas.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_for_upgrade(self): args = ['paas_owncloud', '--upgrade'] result = self.invoke_with_exceptions(paas.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['paas.update'][0][1] self.assertEqual(params['upgrade'], True) def test_update_snapshotprofile_conflict(self): args = ['paas_owncloud', '--delete-snapshotprofile', '--snapshotprofile', '7'] result = self.invoke_with_exceptions(paas.update, args) self.assertEqual(result.exit_code, 2) def test_update_password(self): args = ['paas_owncloud', '--password'] result = self.invoke_with_exceptions(paas.update, args, obj=GandiContextHelper(), input='ploki\nploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n\ Updating your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_delete_snapshotprofile(self): args = ['paas_owncloud', '--delete-snapshotprofile'] result = self.invoke_with_exceptions(paas.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) params = self.api_calls['paas.update'][0][1] self.assertFalse('upgrade' in params) def test_update_background(self): args = ['paas_owncloud', '--bg'] result = self.invoke_with_exceptions(paas.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ {'id': 200, 'step': 'WAIT'}""") self.assertEqual(result.exit_code, 0) def test_restart_unknown(self): args = ['unknown_paas'] result = self.invoke_with_exceptions(paas.restart, args) self.assertEqual(result.output, """\ Sorry PaaS instance unknown_paas does not exist Please use one of the following: ['paas_owncloud', 'paas_cozycloud', \ '126276', '163744', 'aa3e0e26f8.url-de-test.ws', 'cloud.cat.lol', \ '187832c2b34.testurl.ws', 'cloud.iheartcli.com', 'cli.sexy'] """) def test_restart_prompt_ko(self): args = ['paas_owncloud'] result = self.invoke_with_exceptions(paas.restart, args, input='N\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure to restart PaaS instance 'paas_owncloud'? [y/N]: N\ """) self.assertEqual(result.exit_code, 0) def test_restart(self): args = ['paas_owncloud', '--force'] result = self.invoke_with_exceptions(paas.restart, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Restarting your PaaS instance. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_restart_background(self): args = ['paas_owncloud', '--force', '--bg'] result = self.invoke_with_exceptions(paas.restart, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ id : 200 step : WAIT""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_sshkey.py0000644000175000017500000000610413164644514023606 0ustar sayounsayoun00000000000000from .base import CommandTestCase from gandi.cli.commands import sshkey class SSHKeyTestCase(CommandTestCase): def test_list(self): args = ['--id'] result = self.invoke_with_exceptions(sshkey.list, args) self.assertEqual(result.output, """\ name : default fingerprint : b3:11:67:10:2e:1b:a5:66:ed:16:24:98:3e:2e:ed:f5 id : 134 ---------- name : mysecretkey fingerprint : 09:11:21:e3:90:3c:7d:d5:06:d9:6f:f9:36:e1:99:a6 id : 141 """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['default', '--id', '--value'] result = self.invoke_with_exceptions(sshkey.info, args) self.assertEqual(result.output, """\ name : default fingerprint : b3:11:67:10:2e:1b:a5:66:ed:16:24:98:3e:2e:ed:f5 id : 134 value : ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC63QZAW3tusdv+JuyzOoXTND9/wxKogMwZbxBPPtoN7Hjnyn0kUUHMJ6ji5xpbatRYKOeGAoZDW2TXojvbJdQj7tWsRr7ES0qB9qhDGVSDIJWRQ6f9MQCCLjV5tpBTAwb unknown@lol.cat """) # noqa self.assertEqual(result.exit_code, 0) def test_delete(self): args = ['mysecretkey'] result = self.invoke_with_exceptions(sshkey.delete, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_create_value_and_filename_ko(self): args = ['--name', 'newkey', '--value', 'ssh-rsa LjV5tpBTAwb unknown@inter.net', '--filename', 'sandbox/example.txt'] content = """\ ssh-rsa LjV5tpBTAwb unknown@inter.net """ result = self.isolated_invoke_with_exceptions(sshkey.create, args, temp_content=content) self.assertEqual(result.output, """\ Usage: create [OPTIONS] Error: You must not set value AND filename. """) self.assertEqual(result.exit_code, 2) def test_create_no_value_and_no_filename_ko(self): args = ['--name', 'newkey'] result = self.invoke_with_exceptions(sshkey.create, args) self.assertEqual(result.output, """\ Usage: create [OPTIONS] Error: You must set value OR filename. """) self.assertEqual(result.exit_code, 2) def test_create_value_ok(self): args = ['--name', 'newkey', '--value', 'ssh-rsa LjV5tpBTAwb unknown@inter.net'] result = self.invoke_with_exceptions(sshkey.create, args) self.assertEqual(result.output, """\ id : 145 name : newkey fingerprint : b3:11:67:10:2e:1b:a5:55:ed:16:24:98:3e:2e:ed:f5 """) self.assertEqual(result.exit_code, 0) def test_create_file_ok(self): args = ['--name', 'newkey', '--filename', 'sandbox/example.txt'] content = """\ ssh-rsa LjV5tpBTAwb unknown@inter.net """ result = self.isolated_invoke_with_exceptions(sshkey.create, args, temp_content=content) self.assertEqual(result.output, """\ id : 145 name : newkey fingerprint : b3:11:67:10:2e:1b:a5:55:ed:16:24:98:3e:2e:ed:f5 """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_webacc.py0000644000175000017500000004513413120224646023522 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import re from .base import CommandTestCase from gandi.cli.commands import webacc class WebaccTestCase(CommandTestCase): def test_list(self): result = self.invoke_with_exceptions(webacc.list, []) self.assertEqual(result.output, """\ name : webacc01 state : running ssl : Disable Vhosts : Backends : \tBackend with ip address 195.142.160.181 no longer exists. \tYou should remove it. \tip : 195.142.160.181 \tport : 80 \tstate : running ---- name : testwebacc state : running ssl : Disable Vhosts : \tvhost : pouet.iheartcli.com \tssl : Disable Backends : \tname : server01 \tip : 95.142.160.181 \tport : 80 \tstate : running """) self.assertEqual(result.exit_code, 0) def test_list_output_json(self): args = ['--format', 'json'] result = self.invoke_with_exceptions(webacc.list, args) self.assertEqual(result.output, """\ [{"datacenter_id": 3, "date_created": "20160115T162658", "id": 12138, \ "name": "webacc01", "probe": {"enable": true, "host": null, "interval": null, \ "method": null, "response": null, "threshold": null, "timeout": null, \ "url": null, "window": null}, "servers": [{"fallback": false, "id": 14988, \ "ip": "195.142.160.181", "port": 80, "rproxy_id": 132691, \ "state": "running"}], "ssl_enable": false, "state": "running", \ "uuid": 12138, "vhosts": []}, {"datacenter_id": 1, \ "date_created": "20160115T162658", "id": 13263, "name": "testwebacc", \ "probe": {"enable": true, "host": "95.142.160.181", "interval": 10, \ "method": "GET", "response": 200, "threshold": 3, "timeout": 5, "url": "/", \ "window": 5}, "servers": [{"fallback": false, "id": 4988, \ "ip": "95.142.160.181", "port": 80, "rproxy_id": 13269, "state": "running"}], \ "ssl_enable": false, "state": "running", "uuid": 13263, \ "vhosts": [{"cert_id": null, "id": 5171, "name": "pouet.iheartcli.com", \ "rproxy_id": 13263, "state": "running"}]}] """) self.assertEqual(result.exit_code, 0) def test_info(self): result = self.invoke_with_exceptions(webacc.info, ['testwebacc']) self.assertEqual(result.output, """\ name : testwebacc state : running datacenter : Equinix Paris ssl : Disable algorithm : client-ip Vhosts : \tvhost : pouet.iheartcli.com \tssl : None Backends : \tname : server01 \tip : 95.142.160.181 \tport : 80 \tstate : running Probe : \tstate : Enabled \thost : 95.142.160.181 \tinterval : 10 \tmethod : GET \tresponse : 200 \tthreshold : 3 \ttimeout : 5 \turl : / \twindow : 5 """) self.assertEqual(result.exit_code, 0) def test_info_output_json(self): args = ['testwebacc', '--format', 'json'] result = self.invoke_with_exceptions(webacc.info, args) self.assertEqual(result.output, """\ {"datacenter": {"country": "France", "dc_code": "FR-SD2", "id": 1, \ "iso": "FR", "name": "Equinix Paris"}, "date_created": "20160115T162658", \ "id": 13263, "lb": {"algorithm": "client-ip"}, "name": "testwebacc", \ "probe": {"enable": true, "host": "95.142.160.181", "interval": 10, \ "method": "GET", "response": 200, "threshold": 3, "timeout": 5, "url": "/", \ "window": 5}, "servers": [{"fallback": false, "id": 4988, \ "ip": "95.142.160.181", "port": 80, "rproxy_id": 13269, "state": "running"}], \ "ssl_enable": false, "state": "running", "uuid": 13263, \ "vhosts": [{"cert_id": null, "id": 5171, "name": "pouet.iheartcli.com", \ "rproxy_id": 13263, "state": "running"}]} """) self.assertEqual(result.exit_code, 0) def test_info_backend_expired(self): result = self.invoke_with_exceptions(webacc.info, ['webacc01']) self.assertEqual(result.output, """\ name : webacc01 state : running datacenter : Equinix Paris ssl : Disable algorithm : client-ip Vhosts : Backends : \tBackend with ip address 195.142.160.181 no longer exists. \tYou should remove it. \tip : 195.142.160.181 \tport : 80 \tstate : running Probe : \tstate : Enabled \thost : \tinterval : \tmethod : \tresponse : \tthreshold : \ttimeout : \turl : \twindow : """) self.assertEqual(result.exit_code, 0) def test_create(self): args = ['webacc2', '--datacenter', 'FR-SD3'] result = self.invoke_with_exceptions(webacc.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your webaccelerator webacc2 \rProgress: [###] 100.00% 00:00:00 \ \nYour webaccelerator have been created""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.create'][0][0] self.assertEqual(params['name'], 'webacc2') self.assertEqual(params['datacenter_id'], 4) self.assertEqual(params['ssl_enable'], False) self.assertEqual(params['lb'], {'algorithm': 'client-ip'}) self.assertEqual(params['zone_alter'], False) self.assertEqual(params['override'], True) def test_create_datacenter_limited(self): args = ['webacc2', '--datacenter', 'FR-SD2'] result = self.invoke_with_exceptions(webacc.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider using \ another datacenter. Creating your webaccelerator webacc2 \rProgress: [###] 100.00% 00:00:00 \ \nYour webaccelerator have been created""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.create'][0][0] self.assertEqual(params['name'], 'webacc2') self.assertEqual(params['datacenter_id'], 1) self.assertEqual(params['ssl_enable'], False) self.assertEqual(params['lb'], {'algorithm': 'client-ip'}) self.assertEqual(params['zone_alter'], False) self.assertEqual(params['override'], True) def test_create_datacenter_closed(self): args = ['webacc2', '--datacenter', 'US-BA1'] result = self.invoke_with_exceptions(webacc.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_create_ssl_ok(self): args = ['webacc2', '--datacenter', 'FR-SD3', '--vhost', 'pouet.lol.cat', '--ssl'] result = self.invoke_with_exceptions(webacc.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please give the private key for certificate id 710 (CN: lol.cat)""") self.assertEqual(result.exit_code, 0) def test_create_backend_prompt(self): args = ['webacc2', '--datacenter', 'FR-SD3', '--backend', '195.142.160.181'] result = self.invoke_with_exceptions(webacc.create, args, input='8080\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please set a port for backends. \ If you want to set different port for each backend, use `-b ip:port`: 8080 Creating your webaccelerator webacc2 \rProgress: [###] 100.00% 00:00:00 \ \nYour webaccelerator have been created""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.create'][0][0] self.assertEqual(params['name'], 'webacc2') self.assertEqual(params['datacenter_id'], 4) self.assertEqual(params['ssl_enable'], False) self.assertEqual(params['lb'], {'algorithm': 'client-ip'}) self.assertEqual(params['zone_alter'], False) self.assertEqual(params['override'], True) self.assertEqual(params['servers'], ({'ip': u'195.142.160.181', 'port': 8080},)) def test_create_backend_port_ok(self): args = ['webacc2', '--datacenter', 'FR-SD3', '--backend', '195.142.160.181', '--port', 9000] result = self.invoke_with_exceptions(webacc.create, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Creating your webaccelerator webacc2 \rProgress: [###] 100.00% 00:00:00 \ \nYour webaccelerator have been created""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.create'][0][0] self.assertEqual(params['name'], 'webacc2') self.assertEqual(params['datacenter_id'], 4) self.assertEqual(params['ssl_enable'], False) self.assertEqual(params['lb'], {'algorithm': 'client-ip'}) self.assertEqual(params['zone_alter'], False) self.assertEqual(params['override'], True) self.assertEqual(params['servers'], ({'ip': u'195.142.160.181', 'port': 9000},)) def test_update(self): args = ['testwebacc', '-n', 'testwebacc2', '--ssl-enable', '--algorithm', 'round-robin'] result = self.invoke_with_exceptions(webacc.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your webaccelerator \rProgress: [###] 100.00% 00:00:00 \ \nThe webaccelerator have been udated""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.update'][0][1] self.assertEqual(params['name'], 'testwebacc2') self.assertEqual(params['ssl_enable'], True) self.assertEqual(params['lb'], {'algorithm': 'round-robin'}) def test_delete_webacc(self): args = ['-w', 'webacc01'] result = self.invoke_with_exceptions(webacc.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Deleting your webaccelerator named webacc01 \rProgress: [###] 100.00% 00:00:00 \ \nWebaccelerator have been deleted""") self.assertEqual(result.exit_code, 0) def test_delete_vhost(self): args = ['-v', 'pouet.iheartcli.com'] result = self.invoke_with_exceptions(webacc.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Deleting your virtual host pouet.iheartcli.com \rProgress: [###] 100.00% 00:00:00 \ \nYour virtual host have been removed""") self.assertEqual(result.exit_code, 0) def test_delete_backend_prompt(self): args = ['--backend', '195.142.160.181'] result = self.invoke_with_exceptions(webacc.delete, args, input='8080\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please set a port for backends. \ If you want to different port for each backend, use `-b ip:port`: 8080 Removing backend 195.142.160.181:8080 into webaccelerator \rProgress: [###] 100.00% 00:00:00 \ \nYour backend have been removed""") self.assertEqual(result.exit_code, 0) def test_delete_backend_port_ok(self): args = ['--backend', '195.142.160.181', '--port', 9000] result = self.invoke_with_exceptions(webacc.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Removing backend 195.142.160.181:9000 into webaccelerator \rProgress: [###] 100.00% 00:00:00 \ \nYour backend have been removed""") self.assertEqual(result.exit_code, 0) def test_enable_probe(self): args = ['webacc01', '-p'] result = self.invoke_with_exceptions(webacc.enable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Activating probe on webacc01 \rProgress: [###] 100.00% 00:00:00 \ \nThe probe have been activated""") self.assertEqual(result.exit_code, 0) def test_enable_probe_no_resource(self): args = ['-p'] result = self.invoke_with_exceptions(webacc.enable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ You need to indicate the Webaccelerator name""") self.assertEqual(result.exit_code, 0) def test_enable_backend_prompt(self): args = ['webacc01', '--backend', '195.142.160.181'] result = self.invoke_with_exceptions(webacc.enable, args, input='8080\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please set a port for backends. \ If you want to different port for each backend, use `-b ip:port`: 8080 Activating backend 195.142.160.181 \rProgress: [###] 100.00% 00:00:00 \ \nBackend activated""") self.assertEqual(result.exit_code, 0) def test_enable_backend_port_ok(self): args = ['webacc01', '--backend', '195.142.160.181', '--port', 9000] result = self.invoke_with_exceptions(webacc.enable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Activating backend 195.142.160.181 \rProgress: [###] 100.00% 00:00:00 \ \nBackend activated""") self.assertEqual(result.exit_code, 0) def test_disable_probe(self): args = ['webacc01', '-p'] result = self.invoke_with_exceptions(webacc.disable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Desactivating probe on webacc01 \rProgress: [###] 100.00% 00:00:00 \ \nThe probe have been desactivated""") self.assertEqual(result.exit_code, 0) def test_disable_probe_no_resource(self): args = ['-p'] result = self.invoke_with_exceptions(webacc.disable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ You need to indicate the Webaccelerator name""") self.assertEqual(result.exit_code, 0) def test_disable_backend_port_ok(self): args = ['webacc01', '--backend', '195.142.160.181', '--port', 9000] result = self.invoke_with_exceptions(webacc.disable, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Desactivating backend on server 195.142.160.181 \rProgress: [###] 100.00% 00:00:00 \ \nBackend desactivated""") self.assertEqual(result.exit_code, 0) def test_disable_backend_prompt(self): args = ['webacc01', '--backend', '195.142.160.181'] result = self.invoke_with_exceptions(webacc.disable, args, input='8080\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please set a port for backends. \ If you want to different port for each backend, use `-b ip:port`: 8080 Desactivating backend on server 195.142.160.181 \rProgress: [###] 100.00% 00:00:00 \ \nBackend desactivated""") self.assertEqual(result.exit_code, 0) def test_add_vhost(self): args = ['webacc01', '-v', 'pouet.iheartcli.com', '--zone-alter', '--ssl'] result = self.invoke_with_exceptions(webacc.add, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ There is no certificate for pouet.iheartcli.com. Create the certificate with (for exemple) : $ gandi certificate create --cn pouet.iheartcli.com --type std \ \nOr relaunch the current command with --poll-cert option Adding your virtual host (pouet.iheartcli.com) into webacc01 \rProgress: [###] 100.00% 00:00:00 \ \nYour virtual host habe been added""") self.assertEqual(result.exit_code, 0) params = self.api_calls['hosting.rproxy.vhost.create'][0][1] self.assertEqual(params['vhost'], 'pouet.iheartcli.com') self.assertEqual(params['zone_alter'], True) def test_add_vhost_ssl_ok(self): args = ['webacc01', '-v', 'pouet.lol.cat', '--zone-alter', '--ssl'] result = self.invoke_with_exceptions(webacc.add, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please give the private key for certificate id 710 (CN: lol.cat)""") self.assertEqual(result.exit_code, 0) def test_add_backend_prompt(self): args = ['webacc01', '-b', '195.142.160.181'] result = self.invoke_with_exceptions(webacc.add, args, input='80\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Please set a port for backends. \ If you want to different port for each backend, use `-b ip:port`: 80 Adding backend 195.142.160.181:80 into webaccelerator \rProgress: [###] 100.00% 00:00:00 \ \nBackend added""") self.assertEqual(result.exit_code, 0) def test_add_backend_port_ok(self): args = ['webacc01', '-b', '195.142.160.181', '--port', 9000] result = self.invoke_with_exceptions(webacc.add, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Adding backend 195.142.160.181:9000 into webaccelerator \rProgress: [###] 100.00% 00:00:00 \ \nBackend added""") self.assertEqual(result.exit_code, 0) def test_probe_test(self): args = ['webacc01', '--window', 5, '--timeout', 5, '--threshold', 3, '--interval', 10, '--host', '95.142.160.181', '--url', '/', '--http-method', 'GET', '--http-response', 200, '--test'] result = self.invoke_with_exceptions(webacc.probe, args) self.assertEqual(result.output.strip(), """\ status : 200 timeout : 1.0""") self.assertEqual(result.exit_code, 0) def test_probe_update(self): args = ['webacc01', '--window', 5, '--timeout', 5, '--threshold', 3, '--interval', 10, '--host', '95.142.160.181', '--url', '/', '--http-method', 'GET', '--http-response', 200] result = self.invoke_with_exceptions(webacc.probe, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Progress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_vm.py0000644000175000017500000012067113227371252022724 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals import re import socket from ..compat import mock from ..fixtures.mocks import MockObject from .base import CommandTestCase from gandi.cli.commands import vm from gandi.cli.core.base import GandiContextHelper class VmTestCase(CommandTestCase): mocks = [('gandi.cli.core.base.GandiModule.exec_output', MockObject.exec_output)] def test_list(self): result = self.invoke_with_exceptions(vm.list, []) self.assertEqual(result.output, """hostname : vm1426759833 state : running ---------- hostname : vm1426759844 state : running ---------- hostname : server01 state : running ---------- hostname : server02 state : halted """) self.assertEqual(result.exit_code, 0) def test_list_id(self): result = self.invoke_with_exceptions(vm.list, ['--id']) self.assertEqual(result.output, """hostname : vm1426759833 state : running id : 152966 ---------- hostname : vm1426759844 state : running id : 152964 ---------- hostname : server01 state : running id : 152967 ---------- hostname : server02 state : halted id : 152968 """) self.assertEqual(result.exit_code, 0) def test_list_filter_state(self): result = self.invoke_with_exceptions(vm.list, ['--state', 'halted']) self.assertEqual(result.output, """hostname : server02 state : halted """) self.assertEqual(result.exit_code, 0) def test_list_filter_datacenter(self): result = self.invoke_with_exceptions(vm.list, ['--datacenter', 'FR']) self.assertEqual(result.output, """hostname : server01 state : running ---------- hostname : server02 state : halted """) self.assertEqual(result.exit_code, 0) def test_info_ko_resource(self): result = self.invoke_with_exceptions(vm.info, []) self.assertEqual(result.exit_code, 2) def test_info_ok_one_resource(self): result = self.invoke_with_exceptions(vm.info, ['server01']) self.assertEqual(result.output, """hostname : server01 state : running cores : 1 memory : 256 console : datacenter : FR-SD2 ---------- bandwidth : 102400.0 ip4 : 95.142.160.181 ip6 : 2001:4b98:dc0:47:216:3eff:feb2:3862 label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_server01 size : 3072 """) self.assertEqual(result.exit_code, 0) def test_info_ok_multiple_resources(self): args = ['server01', 'vm1426759833'] result = self.invoke_with_exceptions(vm.info, args) self.assertEqual(result.output, """hostname : server01 state : running cores : 1 memory : 256 console : datacenter : FR-SD2 ---------- bandwidth : 102400.0 ip4 : 95.142.160.181 ip6 : 2001:4b98:dc0:47:216:3eff:feb2:3862 label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_server01 size : 3072 ---------- hostname : vm1426759833 state : running cores : 1 memory : 256 console : datacenter : LU-BI1 ---------- bandwidth : 102400.0 ip6 : 2001:4b98:dc2:43:216:3eff:fece:e25f label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_1426759833 size : 3072 """) self.assertEqual(result.exit_code, 0) def test_info_stat(self): result = self.invoke_with_exceptions(vm.info, ['server01', '--stat']) expected = u"""hostname : server01 state : running cores : 1 memory : 256 console : datacenter : FR-SD2 ---------- bandwidth : 102400.0 ip4 : 95.142.160.181 ip6 : 2001:4b98:dc0:47:216:3eff:feb2:3862 label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_server01 size : 3072 vm network stats in : ▁▁ ▂▁ ▁▁▁▂▂▁▁ ▉▃▁ out : ▁▃ ▉▂ ▆▆▁▃▄▁▁ ▁ ▁▉▅▇ disk network stats read : ▁ ▁▁ ▁ ▉ ▁▃▁ write : ▁ ▁ ▂ ▉▁▁ """ self.assertEqual(result.output, expected) self.assertEqual(result.exit_code, 0) def test_datacenters(self): result = self.invoke_with_exceptions(vm.datacenters, []) self.assertEqual(result.output, """\ iso : FR name : Equinix Paris country : France dc_code : FR-SD2 closing on: 25/12/2017 ---------- iso : US name : Level3 Baltimore country : United States of America dc_code : US-BA1 closing on: 25/12/2016 closed for: vm, paas ---------- iso : LU name : Bissen country : Luxembourg dc_code : LU-BI1 ---------- iso : FR name : France, Paris country : France dc_code : FR-SD3 closed for: paas ---------- iso : FR name : France, Paris country : France dc_code : FR-SD5 closed for: paas """) self.assertEqual(result.exit_code, 0) def test_datacenters_id(self): result = self.invoke_with_exceptions(vm.datacenters, ['--id']) self.assertEqual(result.output, """\ iso : FR name : Equinix Paris country : France dc_code : FR-SD2 id : 1 closing on: 25/12/2017 ---------- iso : US name : Level3 Baltimore country : United States of America dc_code : US-BA1 id : 2 closing on: 25/12/2016 closed for: vm, paas ---------- iso : LU name : Bissen country : Luxembourg dc_code : LU-BI1 id : 3 ---------- iso : FR name : France, Paris country : France dc_code : FR-SD3 id : 4 closed for: paas ---------- iso : FR name : France, Paris country : France dc_code : FR-SD5 id : 5 closed for: paas """) self.assertEqual(result.exit_code, 0) def test_kernels(self): result = self.invoke_with_exceptions(vm.kernels, []) linux_hvm_out = """\ flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw """ linux_out_dc1 = """\ flavor : linux version : 2.6.18 (deprecated) version : 2.6.27-compat-sysfs (deprecated) version : 2.6.32 version : 2.6.27 (deprecated) version : 2.6.32-x86_64 version : 2.6.36 (deprecated) version : 2.6.32-x86_64-grsec version : 2.6.36-x86_64 (deprecated) version : 3.2-i386 version : 3.2-x86_64 version : 3.2-x86_64-grsec version : 3.10-x86_64 version : 3.10-i386 """ linux_out_dc2 = """\ flavor : linux version : 2.6.18 (deprecated) version : 2.6.27-compat-sysfs (deprecated) version : 2.6.32 version : 2.6.27 (deprecated) version : 2.6.32-x86_64 version : 2.6.36 (deprecated) version : 2.6.32-x86_64-grsec version : 2.6.36-x86_64 (deprecated) version : 3.2-i386 version : 3.2-x86_64 version : 3.2-x86_64-grsec version : 3.10-x86_64 version : 3.10-i386 """ linux_out_dc3 = """\ flavor : linux version : 2.6.32 version : 2.6.27 (deprecated) version : 2.6.32-x86_64 version : 2.6.32-x86_64-grsec version : 3.2-i386 version : 3.2-x86_64 version : 3.2-x86_64-grsec version : 3.10-x86_64 version : 3.10-i386 """ self.assertTrue(linux_hvm_out in result.output) self.assertTrue(linux_out_dc1 in result.output) self.assertTrue(linux_out_dc2 in result.output) self.assertTrue(linux_out_dc3 in result.output) self.assertTrue('datacenter : Bissen' in result.output) self.assertTrue('datacenter : Level3 Baltimore' in result.output) self.assertTrue('datacenter : Equinix Paris' in result.output) self.assertEqual(result.exit_code, 0) def test_kernels_match(self): result = self.invoke_with_exceptions(vm.kernels, ['3.10']) linux_out = """\ ---------- flavor : linux version : 3.10-x86_64 version : 3.10-i386 """ self.assertTrue(linux_out in result.output) self.assertTrue('datacenter : Bissen' in result.output) self.assertTrue('datacenter : Level3 Baltimore' in result.output) self.assertTrue('datacenter : Equinix Paris' in result.output) self.assertEqual(result.exit_code, 0) def test_kernels_flavor(self): args = ['--flavor', 'linux-hvm'] result = self.invoke_with_exceptions(vm.kernels, args) self.assertEqual(result.output, """\ datacenter : Equinix Paris ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw datacenter : Level3 Baltimore ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw datacenter : Bissen ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw datacenter : France, Paris ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw datacenter : France, Paris ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw """) self.assertEqual(result.exit_code, 0) def test_kernels_datacenter(self): args = ['--datacenter', 'LU'] result = self.invoke_with_exceptions(vm.kernels, args) linux_out = """\ flavor : linux version : 2.6.32 version : 2.6.27 (deprecated) version : 2.6.32-x86_64 version : 2.6.32-x86_64-grsec version : 3.2-i386 version : 3.2-x86_64 version : 3.2-x86_64-grsec version : 3.10-x86_64 version : 3.10-i386 """ linux_hvm_out = """\ flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw """ self.assertTrue(linux_out in result.output) self.assertTrue(linux_hvm_out in result.output) self.assertTrue('datacenter : Bissen' in result.output) self.assertEqual(result.exit_code, 0) def test_kernels_vm(self): result = self.invoke_with_exceptions(vm.kernels, ['--vm', 'server01']) linux_out = """\ flavor : linux version : 2.6.18 (deprecated) version : 2.6.27-compat-sysfs (deprecated) version : 2.6.32 version : 2.6.27 (deprecated) version : 2.6.32-x86_64 version : 2.6.36 (deprecated) version : 2.6.32-x86_64-grsec version : 2.6.36-x86_64 (deprecated) version : 3.2-i386 version : 3.2-x86_64 version : 3.2-x86_64-grsec version : 3.10-x86_64 version : 3.10-i386 """ linux_hvm_out = """\ flavor : linux-hvm version : 3.12-x86_64 (hvm) version : grub version : raw """ self.assertTrue(linux_out in result.output) self.assertTrue(linux_hvm_out in result.output) self.assertTrue('datacenter : Equinix Paris' in result.output) self.assertEqual(result.exit_code, 0) def test_kernels_all(self): args = ['--vm', 'server01', '--datacenter', 'FR', '--flavor', 'linux-hvm', '3.12'] result = self.invoke_with_exceptions(vm.kernels, args) self.assertEqual(result.output, """\ datacenter : Equinix Paris ---------- flavor : linux-hvm version : 3.12-x86_64 (hvm) """) self.assertEqual(result.exit_code, 0) def test_images(self): self.maxDiff = None result = self.invoke_with_exceptions(vm.images, []) self.assertEqual(result.output, """\ label : Fedora 17 32 bits os_arch : x86-32 kernel_version: 3.2-i386 disk_id : 527489 datacenter : LU-BI1 ---------- label : Fedora 17 64 bits os_arch : x86-64 kernel_version: 3.2-x86_64 disk_id : 527490 datacenter : LU-BI1 ---------- label : OpenSUSE 12.2 32 bits os_arch : x86-32 kernel_version: 3.2-i386 disk_id : 527491 datacenter : LU-BI1 ---------- label : OpenSUSE 12.2 64 bits os_arch : x86-64 kernel_version: 3.2-x86_64 disk_id : 527494 datacenter : LU-BI1 ---------- label : CentOS 5 32 bits os_arch : x86-32 kernel_version: 2.6.32 disk_id : 726224 datacenter : LU-BI1 ---------- label : CentOS 5 64 bits os_arch : x86-64 kernel_version: 2.6.32-x86_64 disk_id : 726225 datacenter : LU-BI1 ---------- label : ArchLinux 32 bits os_arch : x86-32 kernel_version: 3.2-i386 disk_id : 726230 datacenter : LU-BI1 ---------- label : ArchLinux 64 bits os_arch : x86-64 kernel_version: 3.2-x86_64 disk_id : 726233 datacenter : LU-BI1 ---------- label : Debian 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 1401491 datacenter : US-BA1 ---------- label : Debian 7 64 bits (HVM) /!\ DEPRECATED os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 1349810 datacenter : FR-SD2 ---------- label : Debian 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 1401327 datacenter : LU-BI1 ---------- label : Debian 8 (testing) 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3315704 datacenter : FR-SD2 ---------- label : Debian 8 (testing) 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3315992 datacenter : US-BA1 ---------- label : Debian 8 os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316070 datacenter : FR-SD2 ---------- label : Debian 8 os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316070 datacenter : LU-BI1 ---------- label : Debian 8 os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316070 datacenter : FR-SD3 ---------- label : Debian 8 os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316070 datacenter : FR-SD5 ---------- label : Debian 8 (testing) 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316076 datacenter : LU-BI1 ---------- label : Ubuntu 14.04 64 bits LTS (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3315748 datacenter : FR-SD2 ---------- label : Ubuntu 14.04 64 bits LTS (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316144 datacenter : US-BA1 ---------- label : Ubuntu 14.04 64 bits LTS (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316160 datacenter : LU-BI1 ---------- label : CentOS 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 2876292 datacenter : FR-SD2 ---------- label : CentOS 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 4744388 datacenter : US-BA1 ---------- label : CentOS 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 4744392 datacenter : LU-BI1 ---------- label : Debian 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 1401492 datacenter : FR-SD3 ---------- label : Debian 7 64 bits (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 1401492 datacenter : FR-SD5 ---------- label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_1426759833 datacenter : LU-BI1 ---------- label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : sys_server01 datacenter : FR-SD2 ---------- label : kernel_version: name : data datacenter : FR-SD2 ---------- label : Debian 7 64 bits kernel_version: 3.2-x86_64 name : snaptest datacenter : FR-SD2 ---------- label : Debian 7 64 bits (HVM) kernel_version: 3.12-x86_64 (hvm) name : newdisk datacenter : LU-BI1 """) self.assertEqual(result.exit_code, 0) def test_images_all(self): args = ['Ubuntu 14.04', '--datacenter', 'LU'] result = self.invoke_with_exceptions(vm.images, args) self.assertEqual(result.output, """\ label : Ubuntu 14.04 64 bits LTS (HVM) os_arch : x86-64 kernel_version: 3.12-x86_64 (hvm) disk_id : 3316160 datacenter : LU-BI1 """) self.assertEqual(result.exit_code, 0) def test_stop_one(self): result = self.invoke_with_exceptions(vm.stop, ['server01']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Stopping your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_stop_multiple(self): args = ['server01', 'vm1426759833'] result = self.invoke_with_exceptions(vm.stop, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Stopping your Virtual Machine(s) 'server01, vm1426759833'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_stop_background(self): result = self.invoke_with_exceptions(vm.stop, ['server01', '--bg']) self.assertEqual(result.output, """\ id : 200 step : WAIT """) self.assertEqual(result.exit_code, 0) def test_start_one(self): result = self.invoke_with_exceptions(vm.start, ['server01']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Starting your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_start_multiple(self): args = ['server01', 'vm1426759833'] result = self.invoke_with_exceptions(vm.start, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Starting your Virtual Machine(s) 'server01, vm1426759833'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_start_background(self): result = self.invoke_with_exceptions(vm.start, ['server01', '--bg']) self.assertEqual(result.output, """\ id : 200 step : WAIT """) self.assertEqual(result.exit_code, 0) def test_reboot_one(self): result = self.invoke_with_exceptions(vm.reboot, ['server01']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Rebooting your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_reboot_multiple(self): args = ['server01', 'vm1426759833'] result = self.invoke_with_exceptions(vm.reboot, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Rebooting your Virtual Machine(s) 'server01, vm1426759833'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_reboot_background(self): result = self.invoke_with_exceptions(vm.reboot, ['server01', '--bg']) self.assertEqual(result.output, """\ id : 200 step : WAIT """) self.assertEqual(result.exit_code, 0) def test_delete_prompt(self): result = self.invoke_with_exceptions(vm.delete, ['server01']) self.assertEqual(result.output.strip(), """\ Are you sure to delete Virtual Machine 'server01'? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_delete_one(self): result = self.invoke_with_exceptions(vm.delete, ['server01', '-f']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Stopping your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00 \n\ Deleting your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_multiple(self): args = ['server01', 'vm1426759833', '-f'] result = self.invoke_with_exceptions(vm.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Stopping your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00 \n\ Stopping your Virtual Machine(s) 'vm1426759833'. \rProgress: [###] 100.00% 00:00:00 \n\ Deleting your Virtual Machine(s) 'server01, vm1426759833'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_unknown(self): result = self.invoke_with_exceptions(vm.delete, ['server100']) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Sorry virtual machine server100 does not exist Please use one of the following: ['vm1426759833', 'vm1426759844', 'server01', \ 'server02', '152966', '152964', '152967', '152968']""") self.assertEqual(result.exit_code, 0) def test_delete_background_ko(self): args = ['server01', '-f', '--bg'] result = self.invoke_with_exceptions(vm.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Virtual machine not stopped, background option disabled Stopping your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00 \n\ Deleting your Virtual Machine(s) 'server01'. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_delete_background_ok(self): args = ['server02', '-f', '--bg'] result = self.invoke_with_exceptions(vm.delete, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ id : 200 step : WAIT""") self.assertEqual(result.exit_code, 0) def test_update_ok(self): args = ['server01', '--memory', '1024', '--cores', '4'] result = self.invoke_with_exceptions(vm.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your Virtual Machine server01. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_memory(self): args = ['server01', '--memory', '10240', '--cores', '4'] result = self.invoke_with_exceptions(vm.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ memory update must be done offline. reboot machine server01? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_update_memory_reboot(self): args = ['server01', '--memory', '10240', '--cores', '4', '--reboot'] result = self.invoke_with_exceptions(vm.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your Virtual Machine server01. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_background(self): args = ['server01', '--memory', '1024', '--cores', '4', '--bg'] result = self.invoke_with_exceptions(vm.update, args) self.assertEqual(result.output, """\ {'id': 200, 'step': 'WAIT'} """) self.assertEqual(result.exit_code, 0) def test_update_password(self): args = ['server01', '--password'] result = self.invoke_with_exceptions(vm.update, args, input='plokiploki\nplokiploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \nUpdating your Virtual Machine server01. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_update_console(self): args = ['server01', '--console'] result = self.invoke_with_exceptions(vm.update, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Updating your Virtual Machine server01. \rProgress: [###] 100.00% 00:00:00""") self.assertEqual(result.exit_code, 0) def test_console(self): args = ['server01'] result = self.invoke_with_exceptions(vm.console, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ /!\\ Please be aware that if you didn\'t provide a password during creation, \ console service will be unavailable. /!\\ You can use "gandi vm update" command to set a password. /!\\ Use ~. ssh escape key to exit. Updating your Virtual Machine server01. \rProgress: [###] 100.00% 00:00:00 \n\ ssh 95.142.160.181@console.gandi.net""") self.assertEqual(result.exit_code, 0) def test_ssh(self): args = ['admin@server01'] result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Requesting access using: ssh admin@95.142.160.181 ... ssh admin@95.142.160.181""") self.assertEqual(result.exit_code, 0) def test_ssh_wipe_key(self): args = ['admin@server01', '--wipe-key'] with mock.patch('gandi.cli.modules.iaas.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Wiping old key and learning the new one ssh-keygen -R "95.142.160.181" Requesting access using: ssh admin@95.142.160.181 ... ssh admin@95.142.160.181""") self.assertEqual(result.exit_code, 0) def test_ssh_wait(self): args = ['server01', '--wait'] with mock.patch('gandi.cli.modules.iaas.socket', create=True) as mock_socket: mock_socket.return_value = mock.MagicMock(name='socket', spec=socket.socket) result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Waiting for the vm to come online Requesting access using: ssh root@95.142.160.181 ... ssh root@95.142.160.181""") self.assertEqual(result.exit_code, 0) def test_ssh_login(self): args = ['server01', '--login', 'joe'] result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Requesting access using: ssh joe@95.142.160.181 ... ssh joe@95.142.160.181""") self.assertEqual(result.exit_code, 0) def test_ssh_identity(self): args = ['admin@server01', '-i', 'key.pub'] result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Requesting access using: ssh -i key.pub admin@95.142.160.181 ... ssh -i key.pub admin@95.142.160.181""") self.assertEqual(result.exit_code, 0) def test_ssh_args(self): args = ['server01', 'sudo reboot'] result = self.invoke_with_exceptions(vm.ssh, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Requesting access using: ssh root@95.142.160.181 sudo reboot ... ssh root@95.142.160.181 sudo reboot""") self.assertEqual(result.exit_code, 0) def test_create_default_hostname_ok(self): args = ['--hostname', 'server500'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n* root user will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: server500, datacenter: FR-SD5 Creating your Virtual Machine server500. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine server500 has been created.""") self.assertEqual(result.exit_code, 0) def test_create_default_ok(self): args = [] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ password: \nRepeat for confirmation: \n* root user will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD5 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_ip_not_vlan_ko(self): args = ['--hostname', 'server500', '--ip', '10.50.10.10'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n\ --ip can't be used without --vlan.""") self.assertEqual(result.exit_code, 0) def test_create_vlan_ip_ok(self): args = ['--hostname', 'server400', '--vlan', 'vlantest', '--ip', '10.50.10.10'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n* Private only ip vm (can't enable \ emergency web console access). * root user will be created. Creating your iface. \rProgress: [###] 100.00% 00:00:00 \n\ Your iface has been created with the following IP addresses: ip4:\t10.50.10.10 * Configuration used: 1 cores, 256Mb memory, ip private, image Debian 8\ , hostname: server400, datacenter: FR-SD5 Creating your Virtual Machine server400. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine server400 has been created.""") self.assertEqual(result.exit_code, 0) def test_create_login_ok(self): args = ['--login', 'administrator'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ password: \nRepeat for confirmation: \n\ * root and administrator users will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD5 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_background_ok(self): args = ['--hostname', 'server500', '--background'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ password: \nRepeat for confirmation: \n* root user will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: server500, datacenter: FR-SD5 * IAAS backend is now creating your VM and its associated resources in the \ background.""") self.assertEqual(result.exit_code, 0) def test_create_sshkey_ok(self): args = ['--sshkey', 'mysecretkey'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper()) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console \ access). * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD5 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_gen_password_root_ok(self): args = ['--gen-password'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper()) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) output = re.sub(r'vm\d+', 'vm', output) output = re.sub(r'with password .*', 'with password FAKEPASSWORD', output) self.assertEqual(output, """\ * root user will be created. * User root setup with password FAKEPASSWORD * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD5 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_gen_password_user_ok(self): args = ['--gen-password', '--login', 'myuser'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper()) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) output = re.sub(r'vm\d+', 'vm', output) output = re.sub(r'with password .*', 'with password FAKEPASSWORD', output) self.assertEqual(output, """\ * root and myuser users will be created. * Users root and myuser setup with password FAKEPASSWORD * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD5 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_image_deprecated(self): args = ['--image', 'Debian 7 64 bits (HVM)', '--sshkey', 'mysecretkey', '--datacenter', 'FR-SD2'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper()) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider \ using another datacenter. /!\ Image Debian 7 64 bits (HVM) is deprecated and will soon be unavailable. * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console \ access). * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 7 64 bits \ (HVM), hostname: vm, datacenter: FR-SD2 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_dc_code_ok(self): args = ['--datacenter', 'FR-SD3'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ password: \nRepeat for confirmation: \n* root user will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD3 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_create_datacenter_closed(self): args = ['--datacenter', 'US-BA1'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ Error: /!\ Datacenter US-BA1 is closed, please choose another datacenter.""") self.assertEqual(result.exit_code, 1) def test_create_datacenter_limited(self): args = ['--datacenter', 'FR-SD2'] result = self.invoke_with_exceptions(vm.create, args, obj=GandiContextHelper(), input='plokiploki\nplokiploki\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(re.sub(r'vm\d+', 'vm', output), """\ /!\ Datacenter FR-SD2 will be closed on 25/12/2017, please consider \ using another datacenter. password: \nRepeat for confirmation: \n\ * root user will be created. * Configuration used: 1 cores, 256Mb memory, ip v6, image Debian 8\ , hostname: vm, datacenter: FR-SD2 Creating your Virtual Machine vm. \rProgress: [###] 100.00% 00:00:00 \n\ Your Virtual Machine vm has been created.""") self.assertEqual(result.exit_code, 0) def test_migrate_not_available(self): args = ['vm1426759844'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Your VM vm1426759844 cannot be migrated yet. \ Migration will be available when datacenter FR-SD5 is opened.""") self.assertEqual(result.exit_code, 0) def test_migrate_ok(self): args = ['server02', '-f'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ * Starting the migration of VM server02 from datacenter FR-SD2 to LU-BI1 VM migration in progress. \rProgress: [###] 100.00% 00:00:00 \n\ Your VM server02 has been migrated.""") self.assertEqual(result.exit_code, 0) def test_migrate_noconfirm(self): args = ['server02'] result = self.invoke_with_exceptions(vm.migrate, args, input='\n') self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Are you sure you want to migrate VM server02 ? [y/N]:""") self.assertEqual(result.exit_code, 0) def test_migrate_background(self): args = ['server02', '--bg', '-f'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(result.output.strip(), """\ * Starting the migration of VM server02 from datacenter FR-SD2 to LU-BI1 id : 9900 step : WAIT""") self.assertEqual(result.exit_code, 0) def test_migrate_finalize_not_found(self): args = ['server02', '-f', '--finalize'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: Cannot find VM server02 migration operation.""") self.assertEqual(result.exit_code, 1) def test_migrate_finalize_not_needed(self): args = ['server01', '-f', '--finalize'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ Error: VM server01 migration does not need finalization.""") self.assertEqual(result.exit_code, 1) def test_migrate_finalize_ok(self): args = ['vm1426759833', '-f', '--finalize'] result = self.invoke_with_exceptions(vm.migrate, args) self.assertEqual(re.sub(r'\[#+\]', '[###]', result.output.strip()), """\ * Finalizing the migration of VM vm1426759833 from datacenter FR-SD2 to LU-BI1 VM migration in progress. \rProgress: [###] 100.00% 00:00:00 \n\ Your VM vm1426759833 has been migrated.""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_certificate.py0000644000175000017500000007110713120224646024557 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- import re from ..compat import mock from .base import CommandTestCase from gandi.cli.commands import certificate class CertTestCase(CommandTestCase): def test_packages(self): result = self.invoke_with_exceptions(certificate.packages, []) wanted = ("""/!\ "gandi certificate packages" is deprecated. Please use "gandi certificate plans". Description | Name | Max altnames | Type -----------------------+--------------------+--------------+----- Standard Single Domain | cert_std_1_0_0 | 1 | std \ \nStandard Wildcard | cert_std_w_0_0 | 1 | std \ \nStandard Multi Domain | cert_std_3_0_0 | 3 | std \ \nStandard Multi Domain | cert_std_5_0_0 | 5 | std \ \nStandard Multi Domain | cert_std_10_0_0 | 10 | std \ \nStandard Multi Domain | cert_std_20_0_0 | 20 | std \ \nPro Single Domain | cert_pro_1_10_0 | 1 | pro \ \nPro Single Domain | cert_pro_1_100_0 | 1 | pro \ \nPro Single Domain | cert_pro_1_100_SGC | 1 | pro \ \nPro Single Domain | cert_pro_1_250_0 | 1 | pro \ \nPro Wildcard | cert_pro_w_250_0 | 1 | pro \ \nPro Wildcard | cert_pro_w_250_SGC | 1 | pro \ \nBusiness Single Domain | cert_bus_1_250_0 | 1 | bus \ \nBusiness Single Domain | cert_bus_1_250_SGC | 1 | bus \ \nBusiness Multi Domain | cert_bus_3_250_0 | 3 | bus \ \nBusiness Multi Domain | cert_bus_5_250_0 | 5 | bus \ \nBusiness Multi Domain | cert_bus_10_250_0 | 10 | bus \ \nBusiness Multi Domain | cert_bus_20_250_0 | 20 | bus \ \n""") # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_plans(self): result = self.invoke_with_exceptions(certificate.plans, []) wanted = ("""\ Description | Max altnames | Type | Warranty -----------------------+--------------+------+--------- Standard Single Domain | 1 | std | 0 \ \nStandard Wildcard | 1 | std | 0 \ \nStandard Multi Domain | 3 | std | 0 \ \nStandard Multi Domain | 5 | std | 0 \ \nStandard Multi Domain | 10 | std | 0 \ \nStandard Multi Domain | 20 | std | 0 \ \nPro Single Domain | 1 | pro | 10,000 \ \nPro Single Domain | 1 | pro | 100,000 \ \nPro Single Domain | 1 | pro | 100,000 \ \nPro Single Domain | 1 | pro | 250,000 \ \nPro Wildcard | 1 | pro | 250,000 \ \nPro Wildcard | 1 | pro | 250,000 \ \nBusiness Single Domain | 1 | bus | 250,000 \ \nBusiness Single Domain | 1 | bus | 250,000 \ \nBusiness Multi Domain | 3 | bus | 250,000 \ \nBusiness Multi Domain | 5 | bus | 250,000 \ \nBusiness Multi Domain | 10 | bus | 250,000 \ \nBusiness Multi Domain | 20 | bus | 250,000 \ \n""") # noqa self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_list(self): result = self.invoke_with_exceptions(certificate.list, []) self.assertEqual(result.output, """\ cn : mydomain.name plan : Standard Single Domain ---------- cn : bew.web plan : Standard Single Domain ---------- cn : iheartcli.com plan : Pro Single Domain ---------- cn : cat.lol plan : Pro Single Domain ---------- cn : iheartcli.com plan : Business Single Domain ---------- cn : inter.net plan : Business Multi Domain ---------- cn : lol.cat plan : Standard Single Domain """) self.assertEqual(result.exit_code, 0) def test_list_all(self): args = ['--id', '--status', '--dates', '--altnames', '--csr', '--cert'] result = self.invoke_with_exceptions(certificate.list, args) self.assertEqual(result.output, """\ cn : mydomain.name plan : Standard Single Domain id : 701 status : pending date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : bew.web plan : Standard Single Domain id : 771 status : pending date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : iheartcli.com plan : Pro Single Domain id : 709 status : valid date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : cat.lol plan : Pro Single Domain id : 709 status : valid date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : iheartcli.com plan : Business Single Domain id : 769 status : valid date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : inter.net plan : Business Multi Domain id : 706 status : valid date_created : 20140904T14:06:26 date_end : csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- ---------- cn : lol.cat plan : Standard Single Domain id : 710 status : valid date_created : 20150318T00:00:00 date_end : 20160318T00:00:00 csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- cert : \ \n-----BEGIN CERTIFICATE----- MIIE5zCCA8+gAwIBAgIQAkC4TU9JG8wqhf4FCrsNGTANBgkqhkiG9w0BAQsFADBf MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDj...tU6XzbS6/s2D1/N1wWO OCD/V3XAROtKr1a0mtJ8n7SZyzr0j3weRbN7nV24RDQ6d4+GHy5zZstKyDrTknlu yyZuDAAYAQJ+nrL5p1gxVNwj1f5XKFk= -----END CERTIFICATE----- altname : pouet.lol.cat """) self.assertEqual(result.exit_code, 0) def test_info(self): args = ['inter.net', 'bew.web'] result = self.invoke_with_exceptions(certificate.info, args) self.assertEqual(result.output, """cn : inter.net date_created : 20140904T14:06:26 date_end : plan : Business Multi Domain status : valid ---------- cn : bew.web date_created : 20140904T14:06:26 date_end : plan : Standard Single Domain status : pending """) self.assertEqual(result.exit_code, 0) def test_info_all(self): args = ['inter.net', '--id', '--altnames', '--csr', '--cert'] result = self.invoke_with_exceptions(certificate.info, args) self.assertEqual(result.output, """cn : inter.net date_created : 20140904T14:06:26 date_end : plan : Business Multi Domain status : valid id : 706 csr : -----BEGIN CERTIFICATE REQUEST----- MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX...K+I= -----END CERTIFICATE REQUEST----- """) self.assertEqual(result.exit_code, 0) def test_create(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--max-altname', '5', ]) wanted = '''The certificate create operation is 1 You can follow it with: $ gandi certificate follow 1 When the operation is DONE, you can retrieve the .crt with: $ gandi certificate export "domain.tld" ''' self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_package_deprecated(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--package', 'cert_std_1_0_0', ]) wanted = '''\ /!\\ Using --package is deprecated, please replace it by --type (in std, pro \ or bus) and --max-altname to set the max number of altnames. The certificate create operation is 1 You can follow it with: $ gandi certificate follow 1 When the operation is DONE, you can retrieve the .crt with: $ gandi certificate export "domain.tld" ''' self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_no_csr(self): result = self.invoke_with_exceptions(certificate.create, ['--duration', 2, '--max-altname', '5', ]) wanted = """You need a CSR or a CN to create a certificate. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_bad_csr(self): args = ['--csr', 'badpath.csr', '--pk', 'badpath.key', '--package', 'cert_std_3_0_0', '--dcv-method', 'email'] result = self.invoke_with_exceptions(certificate.create, args) wanted = """Unable to parse provided csr: badpath.csr """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_no_package_and_option(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--max-altname', '5', '--package', 'cert_std_1_0_0', ]) wanted = """Please do not use --package at the same time you use \ --type, --max-altname or --warranty. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_warranty(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--max-altname', '5', '--type', 'std', '--warranty', '250', ]) wanted = """The warranty can only be specified for pro certificates. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_no_package(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--max-altname', '5', '--cn', '*.lol.cat', '--altnames', 'pouet.lol.cat', '--altnames', 'grumpf.lol.cat', ]) wanted = """\ You can't have a wildcard with multidomain certificate. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_create_csr_empty(self): args = ['--csr', 'sandbox/example.txt', '--duration', 2, '--max-altname', '5', '--cn', '*.lol.cat', '--altnames', 'pouet.lol.cat', '--altnames', 'grumpf.lol.cat', ] result = self.isolated_invoke_with_exceptions(certificate.create, args, temp_content='') self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_create_wild_altnames(self): csr = '''-----BEGIN CERTIFICATE REQUEST----- MIICWjCCAUICAQAwFTETMBEGA1UEAwwKZG9tYWluLnRsZDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAKYPfDoiWuWDwJb+fZhOHA++9yYy1BbxnY729hSd /P12kw1HeIL5CGIhZLpJrwRQmLPTlJ0VttFaqpNm7mEISr+GMJzEWBTyD8750hbW bXwZBcsWi8AsOsnT+sh/cTKGlJctA346HKU3tLlZsvI4ecfnlIZk5Yefgf+78abz SzSV47gPDUNQvGIzP9QPE4bEFu5NjdxPg3ylaQ5cv8iiWHn4iUCRXlxxNfHmH7xE ysFlsD6KnKjR5eYLKBcATeqopGPi72KlcDn5lmtdWsd9aGSl5KlkKQC497buqjbr H31lMAGAC7At6S7AF5GIT5KGjN6KyPrzUOn7FrhNUcnpUQMCAwEAAaAAMA0GCSqG SIb3DQEBCwUAA4IBAQCBM6wc9DfsI1htRhAz7/RfOIn7kb6LygOSEgfb757My+60 N/WP9ndpmob0PW18B1vXBloZEkO/aNTXCGAIPJXRkeTYVhEE2B7K3pc9IiNmLxXC 3b2cwUjgmNw9wmFZ4AuHqzWHevqix3m7Acpkl5ugcCsTVOX3mx84MSguSC+5AWfm DG0VmOWZ0tWjyZuKgtoXgHnH3whEac+pM7M3J+z94/msO9hnpUOQNt4XALEoONrv +xE1FDGhRJAx9AYOtTBQSFLqKB4D6W2hhDVLirxQuJ/lC/l8tyEu96ggfDRrMXE4 v0L9Vc0443fop+UbFCabF0NWM6rJ31Nlv7s3mQIA -----END CERTIFICATE REQUEST-----''' result = self.invoke_with_exceptions(certificate.create, ['--csr', csr, '--duration', 2, '--max-altname', '5', '--type', 'pro', '--warranty', '250', ]) wanted = """\ Can't find any plan with your params. Please call : "gandi certificate plans". """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_ok(self): args = ['lol.cat'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) wanted = """wrote lol.cat.crt """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_no_cert(self): args = ['bew.web'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_export_invalid(self): args = ['mydomain.name'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) self.assertEqual(result.output, """\ The certificate must be in valid status to be exported (701). """) self.assertEqual(result.exit_code, 0) def test_export_multiple_ok(self): args = ['lol.cat', 'inter.net', '-o', 'pouet.crt'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) wanted = """\ Too many certs found, you must specify which cert you want to export """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_exists(self): args = ['lol.cat'] with mock.patch('gandi.cli.commands.certificate.os.path.isfile', create=True) as mock_isfile: mock_isfile.return_value = True result = self.invoke_with_exceptions(certificate.export, args) wanted = """The file lol.cat.crt already exists. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_nothing(self): args = ['inter.net'] result = self.invoke_with_exceptions(certificate.export, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_export_intermediate_business_ok(self): args = ['inter.net', '-i'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() with mock.patch('gandi.cli.commands.certificate.requests.get', create=True) as mock_get: mock_get.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) wanted = """Business certs do not need intermediates. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_intermediate_ok(self): args = ['lol.cat', '-i'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() with mock.patch('gandi.cli.commands.certificate.requests.get', create=True) as mock_get: mock_get.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) wanted = """wrote lol.cat.crt wrote lol.cat.inter.crt """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_export_intermediate_sgc_ok(self): args = ['cat.lol', '-i'] with mock.patch('gandi.cli.commands.certificate.open', create=True) as mock_open: mock_open.return_value = mock.MagicMock() with mock.patch('gandi.cli.commands.certificate.requests.get', create=True) as mock_get: mock_get.return_value = mock.MagicMock() result = self.invoke_with_exceptions(certificate.export, args) wanted = """wrote cat.lol.inter.crt """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_update(self): args = ['inter.net'] result = self.invoke_with_exceptions(certificate.update, args) wanted = """\ openssl req -new -newkey rsa:2048 -sha256 -nodes -out inter.net.csr \ -keyout inter.net.key -subj "/CN=inter.net" The certificate update operation is 400 You can follow it with: $ gandi certificate follow 400 When the operation is DONE, you can retrieve the .crt with: $ gandi certificate export "inter.net" """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_update_multi(self): args = ['iheartcli.com'] result = self.invoke_with_exceptions(certificate.update, args) wanted = """\ Will not update, iheartcli.com is not precise enough. * cert : 709 * cert : 769 """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_follow(self): args = ['600'] result = self.invoke_with_exceptions(certificate.follow, args) wanted = """\ type : certificate_update step : DONE """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_change_dcv(self): args = ['lol.cat', '--dcv-method', 'dns'] result = self.invoke_with_exceptions(certificate.change_dcv, args) wanted = """\ You have to add these records in your domain zone : 920F78CCE11DA7D9554.lol.cat. 10800 IN CNAME AD6A9D35FF5BD9FB03A41.comodoca.com. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_change_dcv_not_valid(self): args = ['mydomain.name', '--dcv-method', 'dns'] result = self.invoke_with_exceptions(certificate.change_dcv, args) wanted = """\ This certificate operation is not in the good step to update the DCV method. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_change_dcv_multi(self): args = ['iheartcli.com', '--dcv-method', 'dns'] result = self.invoke_with_exceptions(certificate.change_dcv, args) wanted = """\ Will not update, iheartcli.com is not precise enough. * cert : 709 * cert : 769 """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_change_dcv_no_oper(self): args = ['cat.lol', '--dcv-method', 'dns'] result = self.invoke_with_exceptions(certificate.change_dcv, args) wanted = """\ Can not find any operation for this certificate. """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_resend_dcv(self): args = ['lol.cat'] result = self.invoke_with_exceptions(certificate.resend_dcv, args) self.assertEqual(result.output, '') self.assertEqual(result.exit_code, 0) def test_resend_dcv_multi(self): args = ['iheartcli.com'] result = self.invoke_with_exceptions(certificate.resend_dcv, args) wanted = """\ Will not update, iheartcli.com is not precise enough. * cert : 709 * cert : 769 """ self.assertEqual(result.output, wanted) self.assertEqual(result.exit_code, 0) def test_resend_dcv_no_oper(self): args = ['cat.lol'] result = self.invoke_with_exceptions(certificate.resend_dcv, args) self.assertEqual(result.output, """\ Can not find any operation for this certificate. """) self.assertEqual(result.exit_code, 0) def test_resend_dcv_not_email(self): args = ['inter.net'] result = self.invoke_with_exceptions(certificate.resend_dcv, args) self.assertEqual(result.output, """\ This certificate operation is not in email DCV. """) self.assertEqual(result.exit_code, 0) def test_resend_dcv_not_valid(self): args = ['mydomain.name'] result = self.invoke_with_exceptions(certificate.resend_dcv, args) self.assertEqual(result.output, """\ This certificate operation is not in the good step to resend the DCV. """) self.assertEqual(result.exit_code, 0) def test_delete_force(self): args = ['lol.cat', '--force'] result = self.invoke_with_exceptions(certificate.delete, args) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Deleting your certificate. \rProgress: [###] 100.00% 00:00:00 \ \nYour certificate 710 has been deleted.""") self.assertEqual(result.exit_code, 0) def test_delete_multiple(self): args = ['iheartcli.com', '--force'] result = self.invoke_with_exceptions(certificate.delete, args) output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Will not delete, iheartcli.com is not precise enough. * cert : 709 * cert : 769""") self.assertEqual(result.exit_code, 0) def test_delete_prompt_ok(self): args = ['lol.cat'] result = self.invoke_with_exceptions(certificate.delete, args, input='y\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Are you sure to delete the certificate lol.cat? [y/N]: y Deleting your certificate. \rProgress: [###] 100.00% 00:00:00 \ \nYour certificate 710 has been deleted.""") self.assertEqual(result.exit_code, 0) def test_delete_prompt_ko(self): args = ['lol.cat'] result = self.invoke_with_exceptions(certificate.delete, args, input='N\n') output = re.sub(r'\[#+\]', '[###]', result.output.strip()) self.assertEqual(output, """\ Are you sure to delete the certificate lol.cat? [y/N]: N""") self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/commands/test_account.py0000644000175000017500000000101212746404125023722 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- from .base import CommandTestCase from gandi.cli.commands import account class AccountTestCase(CommandTestCase): def test_info(self): result = self.invoke_with_exceptions(account.info, []) self.assertEqual(result.output, """\ handle : PXP561-GANDI prepaid : 1337.42 EUR credits : available: 2335360 usage : 633/h time left: 0 year(s) 4 month(s) 29 day(s) 17 hour(s) """) self.assertEqual(result.exit_code, 0) gandi.cli-1.2/gandi/cli/tests/fixtures/0000755000175000017500000000000013227415174020734 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/fixtures/mocks.py0000644000175000017500000000164412623566662022437 0ustar sayounsayoun00000000000000from __future__ import print_function from datetime import datetime class MockObject(object): @classmethod def blank_func(cls, *args, **kwargs): pass @classmethod def execute(cls, command, shell=True): """ Execute a shell command. """ if not shell: print(' '.join(command)) else: print(command) return True @classmethod def exec_output(cls, command, shell=True, encoding='utf-8'): """ Return execution output :param encoding: charset used to decode the stdout :type encoding: str :return: the return of the command :rtype: unicode string """ if not shell: return ' '.join(command) return command @classmethod def now(cls, *args, **kwargs): return datetime(2020, 12, 25, 0, 0, 0) @classmethod def deprecated(cls, message): pass gandi.cli-1.2/gandi/cli/tests/fixtures/api.py0000644000175000017500000000157012623134755022064 0ustar sayounsayoun00000000000000import logging import importlib log = logging.getLogger(__name__) class Api(object): _calls = {} def request(self, method, apikey, *args, **kwargs): log.info('Calling %s%r %r' % (method, args, kwargs)) modname, func = method.split('.', 1) modname = 'gandi.cli.tests.fixtures._' + modname module = importlib.import_module(modname) func = func.replace('.', '_') if (kwargs.get('dry_run', False) and kwargs.get('return_dry_run', False)): func = func + '_dry_run' try: self._calls.setdefault(method, []).append(args) return getattr(module, func)(*args) except Exception as exc: log.exception('Unexpected Exception %s while calling %s' % (exc, method) ) gandi.cli-1.2/gandi/cli/tests/fixtures/_cert.py0000644000175000017500000004706512753315354022420 0ustar sayounsayoun00000000000000try: # python3 from xmlrpc.client import DateTime except ImportError: # python2 from xmlrpclib import DateTime type_list = list def package_list(options): return [{'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 287, 'id': 1, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_std_1_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 0}, {'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 279, 'id': 2, 'max_domains': 3, 'min_domains': 1, 'name': 'cert_std_3_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 0}, {'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 289, 'id': 3, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_std_w_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 1}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 24, 'id': 4, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_1_10_0', 'sgc': 0, 'trustlogo': 1, 'warranty': 10000, 'wildcard': 0}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 34, 'id': 5, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_1_100_0', 'sgc': 0, 'trustlogo': 1, 'warranty': 100000, 'wildcard': 0}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 317, 'id': 6, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_1_100_SGC', 'sgc': 1, 'trustlogo': 1, 'warranty': 100000, 'wildcard': 0}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 7, 'id': 7, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_1_250_0', 'sgc': 0, 'trustlogo': 1, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 35, 'id': 8, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_w_250_0', 'sgc': 0, 'trustlogo': 1, 'warranty': 250000, 'wildcard': 1}, {'category': {'id': 2, 'name': 'pro'}, 'comodo_id': 323, 'id': 9, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_pro_w_250_SGC', 'sgc': 1, 'trustlogo': 1, 'warranty': 250000, 'wildcard': 1}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 337, 'id': 10, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_bus_1_250_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 338, 'id': 11, 'max_domains': 1, 'min_domains': 1, 'name': 'cert_bus_1_250_SGC', 'sgc': 1, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 279, 'id': 12, 'max_domains': 5, 'min_domains': 1, 'name': 'cert_std_5_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 0}, {'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 279, 'id': 13, 'max_domains': 10, 'min_domains': 1, 'name': 'cert_std_10_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 0}, {'category': {'id': 1, 'name': 'standard'}, 'comodo_id': 279, 'id': 14, 'max_domains': 20, 'min_domains': 1, 'name': 'cert_std_20_0_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 0, 'wildcard': 0}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 410, 'id': 15, 'max_domains': 3, 'min_domains': 1, 'name': 'cert_bus_3_250_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 410, 'id': 16, 'max_domains': 5, 'min_domains': 1, 'name': 'cert_bus_5_250_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 410, 'id': 17, 'max_domains': 10, 'min_domains': 1, 'name': 'cert_bus_10_250_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0}, {'category': {'id': 3, 'name': 'business'}, 'comodo_id': 410, 'id': 18, 'max_domains': 20, 'min_domains': 1, 'name': 'cert_bus_20_250_0', 'sgc': 0, 'trustlogo': 0, 'warranty': 250000, 'wildcard': 0} ] def list(options): ret = [{'trustlogo': False, 'assumed_name': None, 'package': 'cert_std_1_0_0', 'order_number': None, 'altnames': [], 'trustlogo_token': {'mydomain.name': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'cert': None, 'date_end': None, 'status': 'pending', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 701, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'mydomain.name', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'trustlogo': False, 'assumed_name': None, 'package': 'cert_std_1_0_0', 'order_number': None, 'altnames': [], 'trustlogo_token': {'bew.web': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'date_end': None, 'status': 'pending', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 771, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'bew.web', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'trustlogo': False, 'assumed_name': None, 'package': 'cert_pro_1_100_SGC', 'order_number': None, 'altnames': [], 'trustlogo_token': {'iheartcli.com': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'cert': None, 'date_end': None, 'status': 'valid', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 709, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'iheartcli.com', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'trustlogo': False, 'assumed_name': None, 'package': 'cert_pro_1_100_SGC', 'order_number': None, 'altnames': [], 'trustlogo_token': {'cat.lol': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'cert': None, 'date_end': None, 'status': 'valid', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 709, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'cat.lol', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'trustlogo': False, 'assumed_name': None, 'package': 'cert_bus_1_250_0', 'order_number': None, 'altnames': [], 'trustlogo_token': {'iheartcli.com': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'cert': None, 'date_end': None, 'status': 'valid', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 769, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'iheartcli.com', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'trustlogo': False, 'assumed_name': None, 'package': 'cert_bus_20_250_0', 'order_number': None, 'altnames': [], 'trustlogo_token': {'inter.net': 'adadadadad'}, 'date_incorporation': None, 'card_pay_trustlogo': False, 'contact': 'TEST1-GANDI', 'date_start': None, 'ida_email': None, 'business_category': None, 'cert': None, 'date_end': None, 'status': 'valid', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_updated': DateTime('20140904T14:06:26'), 'software': 2, 'id': 706, 'joi_locality': None, 'date_created': DateTime('20140904T14:06:26'), 'cn': 'inter.net', 'altname': [], 'sha_version': 1, 'middleman': '', 'ida_tel': None, 'ida_fax': None, 'comodo_id': None, 'joi_country': None, 'joi_state': None}, {'altnames': ['pouet.lol.cat'], 'assumed_name': None, 'business_category': None, 'card_pay_trustlogo': False, 'cert': 'MIIE5zCCA8+gAwIBAgIQAkC4TU9JG8wqhf4FCrsNGTANBgkqhkiG9' 'w0BAQsFADBfMQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDj' '...' 'tU6XzbS6/s2D1/N1wWOOCD/V3XAROtKr1a0mtJ8n7SZyzr0j3weRbN' '7nV24RDQ6d4+GHy5zZstKyDrTknluyyZuDAAYAQJ+nrL5p1gxVNwj1' 'f5XKFk=', 'cn': 'lol.cat', 'altname': [], 'comodo_id': 1777348256, 'contact': 'DF2975-GANDI', 'csr': '-----BEGIN CERTIFICATE REQUEST-----\n' 'MIICgzCCAWsCAQAwPjERMA8GA1UEAwwIZ2F1dnIuaX' '...' 'K+I=\n-----END CERTIFICATE REQUEST-----', 'date_created': DateTime('20150318T00:00:00'), 'date_end': DateTime('20160318T00:00:00'), 'date_incorporation': None, 'date_start': DateTime('20150318T00:00:00'), 'date_updated': DateTime('20150318T00:00:00'), 'id': 710, 'ida_email': None, 'ida_fax': None, 'ida_tel': None, 'joi_country': None, 'joi_locality': None, 'joi_state': None, 'middleman': '', 'order_number': 12345678, 'package': 'cert_std_1_0_0', 'sha_version': 2, 'software': 2, 'status': 'valid', 'trustlogo': False, 'trustlogo_token': {'lol.cat': 'ababababa'}}] options.pop('items_per_page', None) def compare(hc, option): if isinstance(option, (type_list, tuple)): return hc in option return hc == option for fkey in options: ret = [hc for hc in ret if compare(hc[fkey], options[fkey])] return ret def info(id): cert = dict([(cert['id'], cert) for cert in list({})]) return cert[id] def create(*args): return {'id': 1} def update(*args): return {'id': 400} def change_dcv(oper_id, dcv_method): return True def resend_dcv(oper_id): return True def get_dcv_params(params): return { 'sha1': 'AD6A9D35FF5BD9FB03A41F4F82CAA4B77', 'dcv_method': 'dns', 'fqdns': ['lol.cat'], 'message': ['920F78CCE11DA7D9554.lol.cat. 10800 IN ' 'CNAME AD6A9D35FF5BD9FB03A41.comodoca.com.'], 'dns_records': ['920F78CCE11DA7D9554.lol.cat. 10800 IN ' 'CNAME AD6A9D35FF5BD9FB03A41.comodoca.com.'], 'md5': '920F78CCE11DA7ADAD9554', } def delete(cert_id): return {'id': 200, 'step': 'WAIT'} def hosted_list(options): fqdns_id = {'test1.domain.fr': [1, 2], 'test2.domain.fr': [3], 'test3.domain.fr': [4], 'test4.domain.fr': [5], '*.domain.fr': [6]} ret = [{'date_created': DateTime('20150407T00:00:00'), 'date_expire': DateTime('20160316T00:00:00'), 'id': 1, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'SSL/CN=test1.domain.fr'}, {'date_created': DateTime('20150407T00:00:00'), 'date_expire': DateTime('20160316T00:00:00'), 'id': 2, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'SSL/CN=test1.domain.fr'}, {'date_created': DateTime('20150408T00:00:00'), 'date_expire': DateTime('20160408T00:00:00'), 'id': 3, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'SSL/CN=test2.domain.fr'}, {'date_created': DateTime('20150408T00:00:00'), 'date_expire': DateTime('20160408T00:00:00'), 'id': 4, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'SSL/CN=test3.domain.fr'}, {'date_created': DateTime('20150408T00:00:00'), 'date_expire': DateTime('20160408T00:00:00'), 'id': 5, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'SSL/CN=test4.domain.fr'}, {'date_created': DateTime('20150409T00:00:00'), 'date_expire': DateTime('20160409T00:00:00'), 'id': 6, 'state': u'created', 'subject': u'/OU=Domain Control Validated/OU=Gandi Standard ' 'Wildcard SSL/CN=*.domain.fr'}] options.pop('items_per_page', None) fqdns = options.pop('fqdns', None) if fqdns: if not isinstance(fqdns, (type_list, tuple)): fqdns = [fqdns] for fqdn in fqdns: options.setdefault('id', []).extend(fqdns_id.get(fqdn, [])) def compare(hc, option): if isinstance(option, (type_list, tuple)): return hc in option return hc == option for fkey in options: ret = [hc for hc in ret if compare(hc[fkey], options[fkey])] return ret def hosted_info(id): additionals = { 1: {'fqdns': [{'type': u'cn', 'name': u'test1.domain.fr'}], 'related_vhosts': [{'service_id': 1, 'type': 'paas', 'id': 1, 'name': 'test1.domain.fr'}]}, 2: {'fqdns': [{'type': u'cn', 'name': u'test1.domain.fr'}], 'related_vhosts': [{'service_id': 1, 'type': 'paas', 'id': 1, 'name': 'test1.domain.fr'}]}, 3: {'fqdns': [{'type': u'cn', 'name': u'test2.domain.fr'}], 'related_vhosts': []}, 4: {'fqdns': [{'type': u'cn', 'name': u'test3.domain.fr'}], 'related_vhosts': []}, 5: {'fqdns': [{'type': u'cn', 'name': u'test4.domain.fr'}], 'related_vhosts': []}, 6: {'fqdns': [{'type': u'cn', 'name': u'*.domain.fr'}], 'related_vhosts': [{'service_id': 2, 'type': 'paas', 'id': 2, 'name': '*.domain.fr'}]}} def additional(hc): hc.update(additionals[hc['id']]) return hc hc = dict([(hc['id'], additional(hc)) for hc in hosted_list({})]) return hc.get(id) def hosted_create(params): return hosted_info(5) def hosted_delete(id): return gandi.cli-1.2/gandi/cli/tests/fixtures/_paas.py0000644000175000017500000002400312663631621022370 0ustar sayounsayoun00000000000000try: # python3 from xmlrpc.client import DateTime except ImportError: # python2 from xmlrpclib import DateTime def snapshotprofile_list(options): ret = [{'id': 7, 'kept_total': 3, 'name': 'paas_normal', 'quota_factor': 1.3, 'schedules': [{'kept_version': 1, 'name': 'daily'}, {'kept_version': 1, 'name': 'weekly'}, {'kept_version': 1, 'name': 'weekly4'}]}] for fkey in options: ret = [snp for snp in ret if snp[fkey] == options[fkey]] return ret def list(options): ret = [{'catalog_name': 'phpmysql_s', 'console': '1656411@console.dc0.gpaas.net', 'data_disk_additional_size': 0, 'datacenter_id': 1, 'date_end': DateTime('20160408T00:00:00'), 'date_end_commitment': None, 'date_start': DateTime('20130903T22:14:13'), 'id': 126276, 'name': 'paas_owncloud', 'need_upgrade': False, 'servers': [{'id': 126273}], 'size': 's', 'snapshot_profile': None, 'state': 'halted', 'type': 'phpmysql'}, {'catalog_name': 'nodejsmongodb_s', 'console': '185290@console.dc2.gpaas.net', 'data_disk_additional_size': 0, 'datacenter_id': 3, 'date_end': DateTime('20161125T15:52:56'), 'date_end_commitment': DateTime('20151118T18:00:00'), 'date_start': DateTime('20141025T15:52:56'), 'id': 163744, 'name': 'paas_cozycloud', 'need_upgrade': False, 'servers': [{'id': 163728}], 'size': 's', 'snapshot_profile': None, 'state': 'running', 'type': 'nodejsmongodb'}] options.pop('items_per_page', None) for fkey in options: ret = [paas for paas in ret if paas[fkey] == options[fkey]] return ret def info(paas_id): ret = [{'autorenew': None, 'catalog_name': 'nodejsmongodb_s', 'console': '185290@console.dc2.gpaas.net', 'data_disk_additional_size': 0, 'datacenter': {'country': 'Luxembourg', 'dc_code': 'LU-BI1', 'id': 3, 'iso': 'LU', 'name': 'Bissen'}, 'datadisk_total_size': 10.0, 'date_end': DateTime('20161125T15:52:56'), 'date_end_commitment': DateTime('20151118T00:00:00'), 'date_start': DateTime('20141025T15:52:56'), 'ftp_server': 'sftp.dc2.gpaas.net', 'git_server': 'git.dc2.gpaas.net', 'id': 163744, 'name': 'paas_cozycloud', 'need_upgrade': False, 'owner': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'servers': [{'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['']}, 'id': 163728, 'uuid': 19254}], 'size': 's', 'snapshot_profile': None, 'state': 'running', 'type': 'nodejsmongodb', 'user': 185290, 'vhosts': [{'date_creation': DateTime('20141025T15:50:54'), 'id': 1177216, 'name': '187832c2b34.testurl.ws', 'state': 'running'}, {'date_creation': DateTime('20141025T15:50:54'), 'id': 1177220, 'name': 'cloud.iheartcli.com', 'state': 'running'}, {'date_creation': DateTime('20150728T17:50:56'), 'id': 1365951, 'name': 'cli.sexy', 'state': 'running'}]}, {'autorenew': None, 'catalog_name': 'phpmysql_s', 'console': '1656411@console.dc0.gpaas.net', 'data_disk_additional_size': 0, 'datacenter': {'country': 'France', 'dc_code': 'FR-SD2', 'id': 1, 'iso': 'FR', 'name': 'Equinix Paris'}, 'datadisk_total_size': 10.0, 'date_end': DateTime('20160408T00:00:00'), 'date_end_commitment': None, 'date_start': DateTime('20130903T22:14:13'), 'ftp_server': 'sftp.dc0.gpaas.net', 'git_server': 'git.dc0.gpaas.net', 'id': 126276, 'name': 'sap', 'need_upgrade': False, 'owner': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'servers': [{'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['']}, 'id': 126273, 'uuid': 195339}], 'size': 's', 'snapshot_profile': None, 'state': 'halted', 'type': 'phpmysql', 'user': 1656411, 'vhosts': [{'date_creation': DateTime('20130903T22:11:54'), 'id': 160126, 'name': 'aa3e0e26f8.url-de-test.ws', 'state': 'running'}, {'date_creation': DateTime('20130903T22:24:06'), 'id': 160127, 'name': 'cloud.cat.lol', 'state': 'running'}]}, {'autorenew': None, 'catalog_name': 'pythonpgsql_s', 'console': '1185290@console.dc2.gpaas.net', 'data_disk_additional_size': 0, 'datacenter': {'country': 'Luxembourg', 'dc_code': 'LU-BI1', 'id': 3, 'iso': 'LU', 'name': 'Bissen'}, 'datadisk_total_size': 10.0, 'date_end': DateTime('20161125T15:52:56'), 'date_end_commitment': DateTime('20151118T00:00:00'), 'date_start': DateTime('20141025T15:52:56'), 'ftp_server': 'sftp.dc2.gpaas.net', 'git_server': 'git.dc2.gpaas.net', 'id': 123456, 'name': '123456', 'need_upgrade': False, 'owner': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'servers': [{'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['']}, 'id': 1123456, 'uuid': 119254}], 'size': 's', 'snapshot_profile': None, 'state': 'running', 'type': 'pythonpgsql', 'user': 1185290, 'vhosts': [{'date_creation': DateTime('20141025T15:50:54'), 'id': 2177216, 'name': '987832c2b34.testurl.ws', 'state': 'running'}]}] instances = dict([(paas['id'], paas) for paas in ret]) return instances[paas_id] def vhost_list(options): ret = [{'cert_id': None, 'date_creation': DateTime('20130903T22:11:54'), 'id': 160126, 'name': 'aa3e0e26f8.url-de-test.ws', 'paas_id': 126276, 'state': 'running'}, {'cert_id': None, 'date_creation': DateTime('20130903T22:24:06'), 'id': 160127, 'name': 'cloud.cat.lol', 'paas_id': 126276, 'state': 'running'}, {'cert_id': None, 'date_creation': DateTime('20141025T15:50:54'), 'id': 1177216, 'name': '187832c2b34.testurl.ws', 'paas_id': 163744, 'state': 'running'}, {'cert_id': None, 'date_creation': DateTime('20141025T15:50:54'), 'id': 1177220, 'name': 'cloud.iheartcli.com', 'paas_id': 163744, 'state': 'running'}, {'cert_id': None, 'date_creation': DateTime('20150728T17:50:56'), 'id': 1365951, 'name': 'cli.sexy', 'paas_id': 163744, 'state': 'running'}] options.pop('items_per_page', None) for fkey in options: ret = [paas for paas in ret if paas[fkey] == options[fkey]] return ret def vhost_info(name): vhosts = vhost_list({}) vhosts = dict([(vhost['name'], vhost) for vhost in vhosts]) return vhosts[name] def vhost_delete(name): return {'id': 200, 'step': 'WAIT', 'name': 'rproxy_update', 'paas_id': 1177220, 'date_creation': DateTime('20150728T17:50:56')} def type_list(options): ret = [{'database': 'MySQL', 'language': 'PHP', 'name': 'phpmysql'}, {'database': 'PostgreSQL', 'language': 'PHP', 'name': 'phppgsql'}, {'database': 'PostgreSQL', 'language': 'Node.js', 'name': 'nodejspgsql'}, {'database': 'MongoDB', 'language': 'Node.js', 'name': 'nodejsmongodb'}, {'database': 'MySQL', 'language': 'Node.js', 'name': 'nodejsmysql'}, {'database': 'MongoDB', 'language': 'PHP', 'name': 'phpmongodb'}, {'database': 'MySQL', 'language': 'Python', 'name': 'pythonmysql'}, {'database': 'PostgreSQL', 'language': 'Python', 'name': 'pythonpgsql'}, {'database': 'MongoDB', 'language': 'Python', 'name': 'pythonmongodb'}, {'database': 'MySQL', 'language': 'Ruby', 'name': 'rubymysql'}, {'database': 'PostgreSQL', 'language': 'Ruby', 'name': 'rubypgsql'}, {'database': 'MongoDB', 'language': 'Ruby', 'name': 'rubymongodb'}] return ret def update(paas_id, options): return {'id': 200, 'step': 'WAIT'} def vhost_create(options): return {'id': 200, 'step': 'WAIT'} def delete(paas_id): return {'id': 200, 'step': 'WAIT'} def create(options): return {'id': 200, 'step': 'WAIT'} def restart(options): return {'id': 200, 'step': 'WAIT'} gandi.cli-1.2/gandi/cli/tests/fixtures/_hosting.py0000644000175000017500000016737713227142754023146 0ustar sayounsayoun00000000000000from datetime import datetime try: # python3 from xmlrpc.client import DateTime except ImportError: # python2 from xmlrpclib import DateTime def image_list(options): ret = [{'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 527489, 'id': 131, 'kernel_version': '3.2-i386', 'label': 'Fedora 17 32 bits', 'os_arch': 'x86-32', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 527490, 'id': 132, 'kernel_version': '3.2-x86_64', 'label': 'Fedora 17 64 bits', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 527491, 'id': 133, 'kernel_version': '3.2-i386', 'label': 'OpenSUSE 12.2 32 bits', 'os_arch': 'x86-32', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 527494, 'id': 134, 'kernel_version': '3.2-x86_64', 'label': 'OpenSUSE 12.2 64 bits', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 726224, 'id': 149, 'kernel_version': '2.6.32', 'label': 'CentOS 5 32 bits', 'os_arch': 'x86-32', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 726225, 'id': 150, 'kernel_version': '2.6.32-x86_64', 'label': 'CentOS 5 64 bits', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 726230, 'id': 151, 'kernel_version': '3.2-i386', 'label': 'ArchLinux 32 bits', 'os_arch': 'x86-32', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20130902T15:04:18'), 'date_updated': DateTime('20130903T12:14:30'), 'disk_id': 726233, 'id': 152, 'kernel_version': '3.2-x86_64', 'label': 'ArchLinux 64 bits', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 2, 'date_created': DateTime('20140417T18:38:53'), 'date_updated': DateTime('20141030T10:38:45'), 'disk_id': 1401491, 'id': 161, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 1, 'date_created': DateTime('20140417T18:38:53'), 'date_updated': DateTime('20141030T18:06:44'), 'disk_id': 1349810, 'id': 162, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'deprecated'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20140417T18:38:53'), 'date_updated': DateTime('20141030T10:38:45'), 'disk_id': 1401327, 'id': 167, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 1, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3315704, 'id': 172, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8 (testing) 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 2, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3315992, 'id': 176, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8 (testing) 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 1, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316070, 'id': 178, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316070, 'id': 178, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 4, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316070, 'id': 178, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 5, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316070, 'id': 178, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316076, 'id': 180, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 8 (testing) 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 1, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3315748, 'id': 184, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Ubuntu 14.04 64 bits LTS (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 2, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316144, 'id': 188, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Ubuntu 14.04 64 bits LTS (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': DateTime('20141203T14:15:28'), 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 3316160, 'id': 192, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Ubuntu 14.04 64 bits LTS (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 1, 'date_created': None, 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 2876292, 'id': 196, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'CentOS 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 2, 'date_created': None, 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 4744388, 'id': 200, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'CentOS 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 3, 'date_created': None, 'date_updated': DateTime('20150116T11:24:56'), 'disk_id': 4744392, 'id': 204, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'CentOS 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 4, 'date_created': DateTime('20140417T18:38:53'), 'date_updated': DateTime('20141030T10:38:45'), 'disk_id': 1401492, 'id': 163, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, {'author_id': 248842, 'datacenter_id': 5, 'date_created': DateTime('20140417T18:38:53'), 'date_updated': DateTime('20141030T10:38:45'), 'disk_id': 1401492, 'id': 163, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'os_arch': 'x86-64', 'size': 3072, 'visibility': 'all'}, ] for fkey in options: ret = [dc for dc in ret if dc[fkey] == options[fkey]] return ret def datacenter_list(options): ret = [{'iso': 'FR', 'name': 'Equinix Paris', 'id': 1, 'can_migrate_to': [4], 'country': 'France', 'deactivate_at': datetime(2017, 12, 25, 0, 0, 0), 'iaas_closed_for': 'NEW', 'paas_closed_for': 'NEW', 'dc_code': 'FR-SD2'}, {'iso': 'US', 'name': 'Level3 Baltimore', 'id': 2, 'can_migrate_to': [], 'country': 'United States of America', 'deactivate_at': datetime(2016, 12, 25, 0, 0, 0), 'iaas_closed_for': 'ALL', 'paas_closed_for': 'ALL', 'dc_code': 'US-BA1'}, {'iso': 'LU', 'name': 'Bissen', 'id': 3, 'can_migrate_to': [], 'country': 'Luxembourg', 'deactivate_at': None, 'iaas_closed_for': 'NONE', 'paas_closed_for': 'NONE', 'dc_code': 'LU-BI1'}, {'iso': 'FR', 'name': 'France, Paris', 'id': 4, 'can_migrate_to': [], 'country': 'France', 'deactivate_at': None, 'iaas_closed_for': 'NONE', 'paas_closed_for': 'ALL', 'dc_code': 'FR-SD3'}, {'iso': 'FR', 'name': 'France, Paris', 'id': 5, 'can_migrate_to': [], 'country': 'France', 'deactivate_at': None, 'iaas_closed_for': 'NONE', 'paas_closed_for': 'ALL', 'dc_code': 'FR-SD5'}] options.pop('sort_by', None) for fkey in options: if (fkey == 'iaas_opened') or (fkey == 'paas_opened'): fkey = '%s_closed_for' % fkey[:4] ret = [dc for dc in ret if dc[fkey] in ['NONE', 'NEW']] else: ret = [dc for dc in ret if dc[fkey] == options[fkey]] return ret def disk_list(options): disks = [{'can_snapshot': True, 'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:58'), 'id': 4969232, 'is_boot_disk': True, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'name': 'sys_1426759833', 'size': 3072, 'snapshot_profile_id': None, 'snapshots_id': [], 'source': 1401327, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': [152966]}, {'can_snapshot': True, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:29'), 'id': 4969249, 'is_boot_disk': True, 'kernel_cmdline': {'console': 'ttyS0', 'nosep': True, 'ro': True, 'root': '/dev/sda'}, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'name': 'sys_server01', 'size': 3072, 'snapshot_profile_id': None, 'snapshots_id': [], 'source': 1349810, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': [152967]}, {'can_snapshot': True, 'datacenter_id': 1, 'date_created': DateTime('20150319T15:39:54'), 'date_updated': DateTime('20150319T15:40:24'), 'id': 4970079, 'is_boot_disk': False, 'kernel_version': None, 'label': None, 'name': 'data', 'size': 3072, 'snapshot_profile_id': 1, 'snapshots_id': [663497], 'source': None, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': [152967]}, {'can_snapshot': False, 'datacenter_id': 1, 'date_created': DateTime('20140826T00:00:00'), 'date_updated': DateTime('20140826T00:00:00'), 'id': 663497, 'is_boot_disk': False, 'kernel_version': '3.2-x86_64', 'label': 'Debian 7 64 bits', 'name': 'snaptest', 'size': 3072, 'snapshot_profile_id': None, 'snapshots_id': [], 'source': 4970079, 'state': 'created', 'total_size': 3072, 'type': 'snapshot', 'visibility': 'private', 'vms_id': []}, {'can_snapshot': True, 'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:58'), 'id': 4969233, 'is_boot_disk': True, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'name': 'newdisk', 'size': 3072, 'snapshot_profile_id': None, 'snapshots_id': [], 'source': 1401327, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': []}] options.pop('items_per_page', None) for fkey in options: ret = [] for disk in disks: if isinstance(options[fkey], list): if disk[fkey] in options[fkey]: ret.append(disk) elif disk[fkey] == options[fkey]: ret.append(disk) disks = ret return disks def disk_info(id): disks = disk_list({}) disks = dict([(disk['id'], disk) for disk in disks]) return disks[id] def disk_update(disk_id, options): return {'id': 200, 'step': 'WAIT'} def disk_delete(disk_id): return {'id': 200, 'step': 'WAIT'} def disk_rollback_from(disk_id): return {'id': 200, 'step': 'WAIT'} def disk_migrate(disk_id, datacenter_id): return {'id': 200, 'step': 'WAIT'} def disk_create_from(options, disk_id): return {'id': 200, 'step': 'WAIT'} def disk_create(options): return {'id': 200, 'step': 'WAIT', 'disk_id': 9000} def vm_migrate(vm_id, finalize=False): return {'id': 9900, 'step': 'WAIT'} def vm_can_migrate(vm_id): if vm_id == 152964: return {'can_migrate': False, 'matched': ['FR-SD5'], 'can_migrate_to': []} return {'can_migrate': True, 'matched': ['LU-BI1'], 'can_migrate_to': ['LU-BI1']} def vm_list(options): ret = [{'ai_active': 0, 'console': 0, 'cores': 1, 'datacenter_id': 3, 'date_created': DateTime('20141008T16:13:59'), 'date_updated': DateTime('20150319T11:11:31'), 'description': None, 'disks_id': [4969232], 'flex_shares': 0, 'hostname': 'vm1426759833', 'id': 152966, 'ifaces_id': [156572], 'memory': 256, 'state': 'running', 'vm_max_memory': 2048}, {'ai_active': 0, 'console': 0, 'cores': 1, 'datacenter_id': 3, 'date_created': DateTime('20141008T16:13:59'), 'date_updated': DateTime('20150319T11:11:31'), 'description': None, 'disks_id': [4969232], 'flex_shares': 0, 'hostname': 'vm1426759844', 'id': 152964, 'ifaces_id': [156572], 'memory': 256, 'state': 'running', 'vm_max_memory': 2048}, {'ai_active': 0, 'console': 0, 'cores': 1, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:55'), 'description': None, 'disks_id': [4969249], 'flex_shares': 0, 'hostname': 'server01', 'id': 152967, 'ifaces_id': [156573], 'memory': 256, 'state': 'running', 'vm_max_memory': 2048}, {'ai_active': 0, 'console': 0, 'cores': 1, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:55'), 'description': None, 'disks_id': [4969250], 'flex_shares': 0, 'hostname': 'server02', 'id': 152968, 'ifaces_id': [156574], 'memory': 256, 'state': 'halted', 'vm_max_memory': 2048}] options.pop('items_per_page', None) for fkey in options: ret = [vm for vm in ret if vm[fkey] == options[fkey]] return ret def vm_info(id): ret = [{'ai_active': 0, 'console': 0, 'console_url': 'console.gandi.net', 'cores': 1, 'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:11:31'), 'description': None, 'disks': [{'can_snapshot': True, 'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:58'), 'id': 4969232, 'is_boot_disk': True, 'kernel_cmdline': {'console': 'ttyS0', 'nosep': True, 'ro': True, 'root': '/dev/sda'}, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'name': 'sys_1426759833', 'size': 3072, 'snapshot_profile': None, 'snapshots_id': [], 'source': 1401327, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': [152966]}], 'disks_id': [4969232], 'flex_shares': 0, 'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['']}, 'hostname': 'vm1426759833', 'id': 152966, 'ifaces': [{'bandwidth': 102400.0, 'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:35'), 'id': 156572, 'ips': [{'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:36'), 'id': 204557, 'iface_id': 156572, 'ip': '2001:4b98:dc2:43:216:3eff:fece:e25f', 'num': 0, 'reverse': 'xvm6-dc2-fece-e25f.ghst.net', 'state': 'created', 'version': 6}], 'ips_id': [204557], 'num': 0, 'state': 'used', 'type': 'public', 'vlan': None, 'vm_id': 152966}], 'ifaces_id': [156572], 'memory': 256, 'probes': [], 'state': 'running', 'triggers': [], 'vm_max_memory': 2048}, {'ai_active': 0, 'console': 0, 'console_url': 'console.gandi.net', 'cores': 1, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:55'), 'description': None, 'disks': [{'can_snapshot': True, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:29'), 'id': 4969249, 'is_boot_disk': True, 'kernel_cmdline': {'console': 'ttyS0', 'nosep': True, 'ro': True, 'root': '/dev/sda'}, 'kernel_version': '3.12-x86_64 (hvm)', 'label': 'Debian 7 64 bits (HVM)', 'name': 'sys_server01', 'size': 3072, 'snapshot_profile': None, 'snapshots_id': [], 'source': 1349810, 'state': 'created', 'total_size': 3072, 'type': 'data', 'visibility': 'private', 'vms_id': [152967]}], 'disks_id': [4969249], 'flex_shares': 0, 'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['']}, 'hostname': 'server01', 'id': 152967, 'ifaces': [{'bandwidth': 102400.0, 'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:13'), 'date_updated': DateTime('20150319T11:14:16'), 'id': 156573, 'ips': [{'datacenter_id': 1, 'date_created': DateTime('20150317T16:20:10'), 'date_updated': DateTime('20150319T11:14:13'), 'id': 203968, 'iface_id': 156573, 'ip': '95.142.160.181', 'num': 0, 'reverse': 'xvm-160-181.dc0.ghst.net', 'state': 'created', 'version': 4}, {'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:16'), 'date_updated': DateTime('20150319T11:14:16'), 'id': 204558, 'iface_id': 156573, 'ip': '2001:4b98:dc0:47:216:3eff:feb2:3862', 'num': 1, 'reverse': 'xvm6-dc0-feb2-3862.ghst.net', 'state': 'created', 'version': 6}], 'ips_id': [203968, 204558], 'num': 0, 'state': 'used', 'type': 'public', 'vlan': None, 'vm_id': 152967}], 'ifaces_id': [156573], 'memory': 256, 'probes': [], 'state': 'running', 'triggers': [], 'vm_max_memory': 2048}, {'ai_active': 0, 'console': 0, 'console_url': 'console.gandi.net', 'cores': 1, 'datacenter_id': 4, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162658'), 'description': None, 'disks': [], 'disks_id': [4969250], 'flex_shares': 0, 'graph_urls': {'vcpu': [''], 'vdi': [''], 'vif': ['', '']}, 'hostname': 'server02', 'hvm_state': 'unknown', 'id': 152968, 'ifaces': [{'bandwidth': 102400.0, 'datacenter_id': 4, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162658'), 'id': 1274919, 'ips': [{'datacenter_id': 4, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162658'), 'id': 351155, 'iface_id': 1274919, 'ip': '213.167.231.3', 'num': 0, 'reverse': 'xvm-231-3.sd3.ghst.net', 'state': 'created', 'version': 4}, {'datacenter_id': 4, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162658'), 'id': 352862, 'iface_id': 1274919, 'ip': '2001:4b98:c001:1:216:3eff:fec5:c104', 'num': 1, 'reverse': 'xvm6-c001-fec5-c104.ghst.net', 'state': 'created', 'version': 6}], 'ips_id': [351155, 352862], 'num': 0, 'state': 'used', 'type': 'public', 'vlan': {'id': 717, 'name': 'pouet'}, 'vm_id': 227627}, {'bandwidth': 102400.0, 'datacenter_id': 4, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162658'), 'id': 1416, 'ips': [{'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162702'), 'id': 2361, 'iface_id': 1416, 'ip': '192.168.232.252', 'num': 0, 'reverse': '', 'state': 'created', 'version': 4}], 'ips_id': [2361], 'num': 1, 'state': 'used', 'type': 'private', 'vlan': {'id': 717, 'name': 'pouet'}, 'vm_id': 227627}], 'ifaces_id': [1274919, 1416], 'memory': 236, 'probes': [], 'state': 'halted', 'triggers': [], 'vm_max_memory': 2048}] vms = dict([(vm['id'], vm) for vm in ret]) return vms[id] def metric_query(query): vif_bytes_all = [ {'direction': ['in'], 'metric': 'vif.bytes', 'points': [{'timestamp': '2015-03-18T10:00:00', 'value': 24420.0}, {'timestamp': '2015-03-18T11:00:00', 'value': 22370.0}, {'timestamp': '2015-03-18T12:00:00', 'value': 46680.0}, {'timestamp': '2015-03-18T13:00:00', 'value': 61664.0}, {'timestamp': '2015-03-18T14:00:00', 'value': 142789.0}, {'timestamp': '2015-03-18T15:00:00', 'value': 35633.0}, {'timestamp': '2015-03-18T16:00:00', 'value': 213987.0}, {'timestamp': '2015-03-18T17:00:00', 'value': 80055.0}, {'timestamp': '2015-03-18T18:00:00', 'value': 57690.0}, {'timestamp': '2015-03-18T19:00:00', 'value': 83508.0}, {'timestamp': '2015-03-18T20:00:00', 'value': 115038.0}, {'timestamp': '2015-03-18T21:00:00', 'value': 71923.0}, {'timestamp': '2015-03-18T22:00:00', 'value': 259466.0}, {'timestamp': '2015-03-18T23:00:00', 'value': 301198.0}, {'timestamp': '2015-03-19T00:00:00', 'value': 69579.0}, {'timestamp': '2015-03-19T01:00:00', 'value': 99998.0}, {'timestamp': '2015-03-19T02:00:00', 'value': 53706.0}, {'timestamp': '2015-03-19T03:00:00', 'value': 55539.0}, {'timestamp': '2015-03-19T04:00:00', 'value': 60018.0}, {'timestamp': '2015-03-19T05:00:00', 'value': 23000.0}, {'timestamp': '2015-03-19T06:00:00', 'value': 57812.0}, {'timestamp': '2015-03-19T07:00:00', 'value': 984992.0}, {'timestamp': '2015-03-19T08:00:00', 'value': 315608.0}, {'timestamp': '2015-03-19T09:00:00', 'value': 77852.0}], 'resource_id': 152967, 'resource_type': 'vm', 'type': ['public']}, {'direction': ['out'], 'metric': 'vif.bytes', 'points': [{'timestamp': '2015-03-18T10:00:00', 'value': 5335.0}, {'timestamp': '2015-03-18T11:00:00', 'value': 8763.0}, {'timestamp': '2015-03-18T12:00:00', 'value': 43790.0}, {'timestamp': '2015-03-18T13:00:00', 'value': 73345.0}, {'timestamp': '2015-03-18T14:00:00', 'value': 259536.0}, {'timestamp': '2015-03-18T15:00:00', 'value': 18595.0}, {'timestamp': '2015-03-18T16:00:00', 'value': 751379.0}, {'timestamp': '2015-03-18T17:00:00', 'value': 150840.0}, {'timestamp': '2015-03-18T18:00:00', 'value': 43115.0}, {'timestamp': '2015-03-18T19:00:00', 'value': 593737.0}, {'timestamp': '2015-03-18T20:00:00', 'value': 619675.0}, {'timestamp': '2015-03-18T21:00:00', 'value': 67605.0}, {'timestamp': '2015-03-18T22:00:00', 'value': 300711.0}, {'timestamp': '2015-03-18T23:00:00', 'value': 380400.0}, {'timestamp': '2015-03-19T00:00:00', 'value': 62705.0}, {'timestamp': '2015-03-19T01:00:00', 'value': 100512.0}, {'timestamp': '2015-03-19T02:00:00', 'value': 47963.0}, {'timestamp': '2015-03-19T03:00:00', 'value': 50301.0}, {'timestamp': '2015-03-19T04:00:00', 'value': 48572.0}, {'timestamp': '2015-03-19T05:00:00', 'value': 6263.0}, {'timestamp': '2015-03-19T06:00:00', 'value': 67014.0}, {'timestamp': '2015-03-19T07:00:00', 'value': 777215.0}, {'timestamp': '2015-03-19T08:00:00', 'value': 495497.0}, {'timestamp': '2015-03-19T09:00:00', 'value': 660825.0}], 'resource_id': 152967, 'resource_type': 'vm', 'type': ['public']}] vbd_bytes_all = [ {'direction': ['read'], 'metric': 'vbd.bytes', 'points': [{'timestamp': '2015-03-18T10:00:00', 'value': 13824000.0}, {'timestamp': '2015-03-18T11:00:00', 'value': 5644288.0}, {'timestamp': '2015-03-18T12:00:00', 'value': 0.0}, {'timestamp': '2015-03-18T13:00:00', 'value': 13516800.0}, {'timestamp': '2015-03-18T14:00:00', 'value': 27918336.0}, {'timestamp': '2015-03-18T15:00:00', 'value': 9150464.0}, {'timestamp': '2015-03-18T16:00:00', 'value': 64323584.0}, {'timestamp': '2015-03-18T17:00:00', 'value': 29974528.0}, {'timestamp': '2015-03-18T18:00:00', 'value': 761856.0}, {'timestamp': '2015-03-18T19:00:00', 'value': 41775104.0}, {'timestamp': '2015-03-18T20:00:00', 'value': 14286848.0}, {'timestamp': '2015-03-18T21:00:00', 'value': 1073152.0}, {'timestamp': '2015-03-18T22:00:00', 'value': 387248128.0}, {'timestamp': '2015-03-18T23:00:00', 'value': 13754368.0}, {'timestamp': '2015-03-19T00:00:00', 'value': 2056192.0}, {'timestamp': '2015-03-19T01:00:00', 'value': 9990144.0}, {'timestamp': '2015-03-19T02:00:00', 'value': 643072.0}, {'timestamp': '2015-03-19T03:00:00', 'value': 6148096.0}, {'timestamp': '2015-03-19T04:00:00', 'value': 8974336.0}, {'timestamp': '2015-03-19T05:00:00', 'value': 782336.0}, {'timestamp': '2015-03-19T06:00:00', 'value': 12214272.0}, {'timestamp': '2015-03-19T07:00:00', 'value': 29261824.0}, {'timestamp': '2015-03-19T08:00:00', 'value': 144080896.0}, {'timestamp': '2015-03-19T09:00:00', 'value': 39198720.0}], 'resource_id': 152967, 'resource_type': 'vm'}, {'direction': ['write'], 'metric': 'vbd.bytes', 'points': [{'timestamp': '2015-03-18T10:00:00', 'value': 217088.0}, {'timestamp': '2015-03-18T11:00:00', 'value': 229376.0}, {'timestamp': '2015-03-18T12:00:00', 'value': 401408.0}, {'timestamp': '2015-03-18T13:00:00', 'value': 577536.0}, {'timestamp': '2015-03-18T14:00:00', 'value': 3862528.0}, {'timestamp': '2015-03-18T15:00:00', 'value': 217088.0}, {'timestamp': '2015-03-18T16:00:00', 'value': 2363392.0}, {'timestamp': '2015-03-18T17:00:00', 'value': 1773568.0}, {'timestamp': '2015-03-18T18:00:00', 'value': 217088.0}, {'timestamp': '2015-03-18T19:00:00', 'value': 3153920.0}, {'timestamp': '2015-03-18T20:00:00', 'value': 2039808.0}, {'timestamp': '2015-03-18T21:00:00', 'value': 606208.0}, {'timestamp': '2015-03-18T22:00:00', 'value': 12505088.0}, {'timestamp': '2015-03-18T23:00:00', 'value': 675840.0}, {'timestamp': '2015-03-19T00:00:00', 'value': 602112.0}, {'timestamp': '2015-03-19T01:00:00', 'value': 598016.0}, {'timestamp': '2015-03-19T02:00:00', 'value': 483328.0}, {'timestamp': '2015-03-19T03:00:00', 'value': 462848.0}, {'timestamp': '2015-03-19T04:00:00', 'value': 471040.0}, {'timestamp': '2015-03-19T05:00:00', 'value': 487424.0}, {'timestamp': '2015-03-19T06:00:00', 'value': 499712.0}, {'timestamp': '2015-03-19T07:00:00', 'value': 42958848.0}, {'timestamp': '2015-03-19T08:00:00', 'value': 6299648.0}, {'timestamp': '2015-03-19T09:00:00', 'value': 3862528.0}], 'resource_id': 152967, 'resource_type': 'vm'}] vfs_df_bytes_all = [ {'metric': 'vfs.df.bytes', 'points': [{'timestamp': '2015-11-18T07:19:00', 'value': 10679488512.0}, {'timestamp': '2015-11-18T07:20:00'}], 'resource_id': 163744, 'resource_type': 'paas', 'size': ['free']}, {'metric': 'vfs.df.bytes', 'points': [{'timestamp': '2015-11-18T07:19:00', 'value': 57929728.0}, {'timestamp': '2015-11-18T07:20:00'}], 'resource_id': 163744, 'resource_type': 'paas', 'size': ['used']}] webacc_requests_cache_all = [ {'cache': ['miss'], 'metric': 'webacc.requests', 'points': [{'timestamp': '2015-11-17T00:00:00', 'value': 2.0}, {'timestamp': '2015-11-18T00:00:00'}], 'resource_id': 163744, 'resource_type': 'paas', 'status': ['2xx']}] metrics = {'vif.bytes.all': vif_bytes_all, 'vbd.bytes.all': vbd_bytes_all, 'vfs.df.bytes.all': vfs_df_bytes_all, 'webacc.requests.cache.all': webacc_requests_cache_all} metrics = [item for item in metrics[query['query']] if item['resource_id'] == query['resource_id'][0]] return metrics def disk_list_kernels(dc_id): ret = { 1: {'linux': ['2.6.18 (deprecated)', '2.6.27-compat-sysfs (deprecated)', '2.6.32', '2.6.27 (deprecated)', '2.6.32-x86_64', '2.6.36 (deprecated)', '2.6.32-x86_64-grsec', '2.6.36-x86_64 (deprecated)', '3.2-i386', '3.2-x86_64', '3.2-x86_64-grsec', '3.10-x86_64', '3.10-i386'], 'linux-hvm': ['3.12-x86_64 (hvm)', 'grub', 'raw']}, 2: {'linux': ['2.6.18 (deprecated)', '2.6.27-compat-sysfs (deprecated)', '2.6.32', '2.6.27 (deprecated)', '2.6.32-x86_64', '2.6.36 (deprecated)', '2.6.32-x86_64-grsec', '2.6.36-x86_64 (deprecated)', '3.2-i386', '3.2-x86_64', '3.2-x86_64-grsec', '3.10-x86_64', '3.10-i386'], 'linux-hvm': ['3.12-x86_64 (hvm)', 'grub', 'raw']}, 3: {'linux': ['2.6.32', '2.6.27 (deprecated)', '2.6.32-x86_64', '2.6.32-x86_64-grsec', '3.2-i386', '3.2-x86_64', '3.2-x86_64-grsec', '3.10-x86_64', '3.10-i386'], 'linux-hvm': ['3.12-x86_64 (hvm)', 'grub', 'raw']}, 4: {'linux': ['2.6.32', '2.6.27 (deprecated)', '2.6.32-x86_64', '2.6.32-x86_64-grsec', '3.2-i386', '3.2-x86_64', '3.2-x86_64-grsec', '3.10-x86_64', '3.10-i386', '3.12-x86_64'], 'linux-hvm': ['3.12-x86_64 (hvm)', 'grub', 'raw']}} return ret.get(dc_id, ret[4]) def account_info(): return {'average_credit_cost': 0.0, 'credits': 2335360, 'cycle_day': 23, 'date_credits_expiration': DateTime('20160319T10:07:24'), 'fullname': 'Peter Parker', 'handle': 'PXP561-GANDI', 'id': 2920674, 'products': None, 'rating_enabled': True, 'resources': {'available': None, 'expired': None, 'granted': None, 'used': None}, 'share_definition': None} def rating_list(): return [{'bw_out': None, 'cpu': {'default': 168}, 'disk_data': {'default': 135}, 'disk_snapshot': None, 'disk_snapshot_auto': None, 'instance': {'default': 0}, 'ip': {'v4_public': 210, 'v6': 0}, 'ram': {'default': 120}, 'rproxy': None, 'rproxy_server': None, 'rproxy_ssl': None, 'timestamp': DateTime('20150319T15:07:24')}] def vm_disk_detach(vm_id, disk_id): if vm_id == 152967 and disk_id == 4970079: return {'id': 200, 'step': 'WAIT'} def vm_iface_detach(vm_id, iface_id): if vm_id == 152967 and iface_id == 156573: return {'id': 200, 'step': 'WAIT'} def vm_iface_attach(vm_id, iface_id): if vm_id == 152966 and iface_id == 156573: return {'id': 200, 'step': 'WAIT'} if vm_id == 152967 and iface_id == 156572: return {'id': 200, 'step': 'WAIT'} if vm_id == 152967 and iface_id == 156573: return {'id': 200, 'step': 'WAIT', 'iface_id': 156573} def vm_disk_attach(vm_id, disk_id, options): if vm_id == 152967 and disk_id == 663497: return {'id': 200, 'step': 'WAIT'} if vm_id == 152966 and disk_id == 4970079: return {'id': 200, 'step': 'WAIT'} if vm_id == 152967 and disk_id == 9000: return {'id': 200, 'step': 'WAIT'} def vm_stop(vm_id): if vm_id in (152967, 152966): return {'id': 200, 'step': 'WAIT'} def vm_start(vm_id): if vm_id in (152967, 152966): return {'id': 200, 'step': 'WAIT'} def vm_reboot(vm_id): if vm_id in (152967, 152966): return {'id': 200, 'step': 'WAIT'} def vm_delete(vm_id): if vm_id in (152968, 152967, 152966): return {'id': 200, 'step': 'WAIT'} def vm_update(vm_id, options): if vm_id in (152967, 152966): return {'id': 200, 'step': 'WAIT'} def vm_create_from(vm_spec, disk_spec, src_disk_id): return [{'id': 300, 'step': 'WAIT'}] def vlan_list(options): ret = [{'datacenter_id': 1, 'gateway': '10.7.13.254', 'id': 123, 'name': 'vlantest', 'state': 'created', 'subnet': '10.7.13.0/24', 'uuid': 321}, {'datacenter_id': 1, 'gateway': '192.168.232.254', 'id': 717, 'name': 'pouet', 'state': 'created', 'subnet': '192.168.232.0/24', 'uuid': 720}, {'datacenter_id': 4, 'gateway': '10.7.242.254', 'id': 999, 'name': 'intranet', 'state': 'created', 'subnet': '10.7.242.0/24', 'uuid': 421}] options.pop('items_per_page', None) for fkey in options: ret = [vlan for vlan in ret if vlan[fkey] == options[fkey]] return ret def vlan_info(id): vlans = vlan_list({}) vlans = dict([(vlan['id'], vlan) for vlan in vlans]) return vlans[id] def vlan_delete(vlan_id): return {'id': 200, 'step': 'WAIT'} def vlan_create(options): return {'id': 200, 'step': 'WAIT'} def vlan_update(vlan_id, options): return {'id': 200, 'step': 'WAIT'} def iface_create(options): if 'ip' in options: return {'id': 200, 'step': 'WAIT', 'iface_id': 156572} return {'id': 200, 'step': 'WAIT', 'iface_id': 156573} def iface_delete(ip_id): return {'id': 200, 'step': 'WAIT'} def iface_list(options): ret = [{'bandwidth': 102400.0, 'datacenter_id': 1, 'date_created': DateTime('20140423T00:00:00'), 'date_updated': DateTime('20140423T00:00:00'), 'id': 156573, 'ips_id': [203968, 204558], 'ips': [{'datacenter_id': 1, 'date_created': DateTime('20150317T16:20:10'), 'date_updated': DateTime('20150319T11:14:13'), 'id': 203968, 'iface_id': 156573, 'ip': '95.142.160.181', 'num': 0, 'reverse': 'xvm-160-181.dc0.ghst.net', 'state': 'created', 'version': 4}, {'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:16'), 'date_updated': DateTime('20150319T11:14:16'), 'id': 204558, 'iface_id': 156573, 'ip': '2001:4b98:dc0:47:216:3eff:feb2:3862', 'num': 1, 'reverse': 'xvm6-dc0-feb2-3862.ghst.net', 'state': 'created', 'version': 6}], 'num': 0, 'state': 'used', 'type': 'public', 'vlan': None, 'vm_id': 152967}, {'bandwidth': 102400.0, 'datacenter_id': 1, 'date_created': DateTime('20141009T00:00:00'), 'date_updated': DateTime('20141105T00:00:00'), 'id': 1416, 'ips_id': [2361], 'ips': [{'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162702'), 'id': 2361, 'iface_id': 1416, 'ip': '192.168.232.252', 'num': 0, 'reverse': '', 'state': 'created', 'version': 4}], 'num': None, 'state': 'used', 'type': 'private', 'vlan': {'id': 717, 'name': 'pouet'}, 'vm_id': 152968}, {'bandwidth': 204800.0, 'datacenter_id': 1, 'date_created': DateTime('20150105T00:00:00'), 'date_updated': DateTime('20150105T00:00:00'), 'id': 1914, 'ips': [{'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162702'), 'id': 2361, 'iface_id': 1914, 'ip': '192.168.232.253', 'num': 0, 'reverse': '', 'state': 'created', 'version': 4}], 'ips_id': [2361], 'num': None, 'state': 'used', 'type': 'private', 'vlan': {'id': 717, 'name': 'pouet'}, 'vm_id': 152968}, {'bandwidth': 204800.0, 'datacenter_id': 1, 'date_created': DateTime('20150105T00:00:00'), 'date_updated': DateTime('20150105T00:00:00'), 'id': 156572, 'ips_id': [204557], 'ips': [{'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:36'), 'id': 204557, 'iface_id': 156572, 'ip': '10.50.10.10', 'num': 0, 'reverse': 'xvm6-dc2-fece-e25f.ghst.net', 'state': 'created', 'version': 4}], 'num': None, 'state': 'free', 'type': 'private', 'vlan': None, 'vm_id': None}] options.pop('items_per_page', None) for fkey in options: if fkey == 'vlan': ret_ = [] for iface in ret: if iface['vlan'] and iface['vlan']['name'] == options['vlan']: ret_.append(iface) ret = ret_ elif fkey == 'vlan_id': ret_ = [] for iface in ret: if iface['vlan'] and iface['vlan']['id'] == options['vlan_id']: ret_.append(iface) ret = ret_ else: ret = [iface for iface in ret if iface[fkey] == options[fkey]] return ret def iface_info(iface_id): ifaces = iface_list({}) ifaces = dict([(iface['id'], iface) for iface in ifaces]) return ifaces[iface_id] def ip_list(options): ips = [{'datacenter_id': 1, 'date_created': DateTime('20150317T16:20:10'), 'date_updated': DateTime('20150319T11:14:13'), 'id': 203968, 'iface_id': 156573, 'ip': '95.142.160.181', 'num': 0, 'reverse': 'xvm-160-181.dc0.ghst.net', 'state': 'created', 'version': 4}, {'datacenter_id': 3, 'date_created': DateTime('20150319T11:10:34'), 'date_updated': DateTime('20150319T11:10:36'), 'id': 204557, 'iface_id': 156572, 'ip': '2001:4b98:dc2:43:216:3eff:fece:e25f', 'num': 0, 'reverse': 'xvm6-dc2-fece-e25f.ghst.net', 'state': 'created', 'version': 6}, {'datacenter_id': 1, 'date_created': DateTime('20150319T11:14:16'), 'date_updated': DateTime('20150319T11:14:16'), 'id': 204558, 'iface_id': 156573, 'ip': '2001:4b98:dc0:47:216:3eff:feb2:3862', 'num': 1, 'reverse': 'xvm6-dc0-feb2-3862.ghst.net', 'state': 'created', 'version': 6}, {'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162702'), 'id': 2361, 'iface_id': 1914, 'ip': '192.168.232.253', 'num': 0, 'reverse': '', 'state': 'created', 'version': 4}, {'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'date_updated': DateTime('20160115T162702'), 'id': 2361, 'iface_id': 1416, 'ip': '192.168.232.252', 'num': 0, 'reverse': '', 'state': 'created', 'version': 4}] options.pop('items_per_page', None) for fkey in options: ret = [] for ip in ips: if isinstance(options[fkey], list): if ip[fkey] in options[fkey]: ret.append(ip) elif ip[fkey] == options[fkey]: ret.append(ip) ips = ret return ips def ip_info(ip_id): ips = ip_list({}) ips = dict([(ip['id'], ip) for ip in ips]) return ips[ip_id] def ip_update(ip_id, options): return {'id': 200, 'step': 'WAIT'} def ssh_list(options): ret = [{'fingerprint': 'b3:11:67:10:2e:1b:a5:66:ed:16:24:98:3e:2e:ed:f5', 'id': 134, 'name': 'default', 'value': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC63QZAW3tusdv+JuyzOoXTND9/wxKogMwZbxBPPtoN7Hjnyn0kUUHMJ6ji5xpbatRYKOeGAoZDW2TXojvbJdQj7tWsRr7ES0qB9qhDGVSDIJWRQ6f9MQCCLjV5tpBTAwb unknown@lol.cat'}, # noqa {'fingerprint': '09:11:21:e3:90:3c:7d:d5:06:d9:6f:f9:36:e1:99:a6', 'id': 141, 'name': 'mysecretkey'}] options.pop('items_per_page', None) for fkey in options: ret = [vm for vm in ret if vm[fkey] == options[fkey]] return ret def ssh_info(key_id): keys = ssh_list({}) keys = dict([(key['id'], key) for key in keys]) return keys[key_id] def ssh_delete(key_id): return {'id': 200, 'step': 'WAIT'} def ssh_create(params): return {'fingerprint': 'b3:11:67:10:2e:1b:a5:55:ed:16:24:98:3e:2e:ed:f5', 'id': 145, 'name': params['name'], 'value': params['value']} def snapshotprofile_list(options): ret = [{'id': 1, 'kept_total': 2, 'name': 'minimal', 'quota_factor': 1.2, 'schedules': [{'kept_version': 2, 'name': 'daily'}]}, {'id': 2, 'kept_total': 7, 'name': 'full_week', 'quota_factor': 1.7, 'schedules': [{'kept_version': 7, 'name': 'daily'}]}, {'id': 3, 'kept_total': 10, 'name': 'security', 'quota_factor': 2.0, 'schedules': [{'kept_version': 3, 'name': 'hourly6'}, {'kept_version': 6, 'name': 'daily'}, {'kept_version': 1, 'name': 'weekly4'}]}] for fkey in options: ret = [snp for snp in ret if snp[fkey] == options[fkey]] return ret def rproxy_list(options): ret = [{'datacenter_id': 3, 'date_created': DateTime('20160115T162658'), 'id': 12138, 'name': 'webacc01', 'probe': {'enable': True, 'host': None, 'interval': None, 'method': None, 'response': None, 'threshold': None, 'timeout': None, 'url': None, 'window': None}, 'servers': [{'fallback': False, 'id': 14988, 'ip': '195.142.160.181', 'port': 80, 'rproxy_id': 132691, 'state': 'running'}], 'ssl_enable': False, 'state': 'running', 'uuid': 12138, 'vhosts': []}, {'datacenter_id': 1, 'date_created': DateTime('20160115T162658'), 'id': 13263, 'name': 'testwebacc', 'probe': {'enable': True, 'host': '95.142.160.181', 'interval': 10, 'method': 'GET', 'response': 200, 'threshold': 3, 'timeout': 5, 'url': '/', 'window': 5}, 'servers': [{'fallback': False, 'id': 4988, 'ip': '95.142.160.181', 'port': 80, 'rproxy_id': 13269, 'state': 'running'}], 'ssl_enable': False, 'state': 'running', 'uuid': 13263, 'vhosts': [{'cert_id': None, 'id': 5171, 'name': 'pouet.iheartcli.com', 'rproxy_id': 13263, 'state': 'running'}]}] options.pop('items_per_page', None) for fkey in options: ret = [rpx for rpx in ret if rpx[fkey] == options[fkey]] return ret def rproxy_delete(rproxy_id): return {'id': 200, 'step': 'WAIT'} def rproxy_info(rproxy_id): ret = [{'datacenter': {'country': 'France', 'dc_code': 'FR-SD2', 'id': 1, 'iso': 'FR', 'name': 'Equinix Paris'}, 'date_created': DateTime('20160115T162658'), 'id': 13263, 'lb': {'algorithm': 'client-ip'}, 'name': 'testwebacc', 'probe': {'enable': True, 'host': '95.142.160.181', 'interval': 10, 'method': 'GET', 'response': 200, 'threshold': 3, 'timeout': 5, 'url': '/', 'window': 5}, 'servers': [{'fallback': False, 'id': 4988, 'ip': '95.142.160.181', 'port': 80, 'rproxy_id': 13269, 'state': 'running'}], 'ssl_enable': False, 'state': 'running', 'uuid': 13263, 'vhosts': [{'cert_id': None, 'id': 5171, 'name': 'pouet.iheartcli.com', 'rproxy_id': 13263, 'state': 'running'}]}, {'datacenter': {'country': 'France', 'dc_code': 'FR-SD2', 'id': 1, 'iso': 'FR', 'name': 'Equinix Paris'}, 'date_created': DateTime('20160115T162658'), 'id': 12138, 'lb': {'algorithm': 'client-ip'}, 'name': 'webacc01', 'probe': {'enable': True, 'host': None, 'interval': None, 'method': None, 'response': None, 'threshold': None, 'timeout': None, 'url': None, 'window': None}, 'servers': [{'fallback': False, 'id': 14988, 'ip': '195.142.160.181', 'port': 80, 'rproxy_id': 132691, 'state': 'running'}], 'ssl_enable': False, 'state': 'running', 'uuid': 12138, 'vhosts': []}] rpx = dict([(rpx['id'], rpx) for rpx in ret]) return rpx[rproxy_id] def rproxy_update(rproxy_id, params): return {'id': 200, 'step': 'WAIT'} def rproxy_create(params): return {'id': 200, 'step': 'WAIT'} def rproxy_probe_disable(rproxy_id): return {'id': 200, 'step': 'WAIT'} def rproxy_probe_enable(rproxy_id): return {'id': 200, 'step': 'WAIT'} def rproxy_vhost_list(): ret = [{'cert_id': None, 'id': 5177, 'name': 'pouet.iheartcli.com', 'rproxy_id': 13269, 'state': 'running'}] return ret def rproxy_vhost_delete(vhost): return {'id': 200, 'step': 'WAIT'} def rproxy_vhost_create(rproxy_id, vhost): return {'id': 200, 'step': 'WAIT'} def rproxy_probe_test(rproxy_id, params): return {'servers': [{'server': 4988, 'status': 200, 'timeout': 1.0}], 'status': 200, 'timeout': 1.0} def rproxy_probe_update(rproxy_id, params): return {'id': 200, 'step': 'WAIT'} def rproxy_server_create(rproxy_id, params): return {'id': 200, 'step': 'WAIT'} def rproxy_server_list(params): return [{'fallback': False, 'id': 14988, 'ip': '195.142.160.181', 'port': 80, 'rproxy_id': 132691, 'state': 'running'}] def rproxy_server_delete(server_id): return {'id': 200, 'step': 'WAIT'} def rproxy_server_enable(server_id): return {'id': 200, 'step': 'WAIT'} def rproxy_server_disable(server_id): return {'id': 200, 'step': 'WAIT'} gandi.cli-1.2/gandi/cli/tests/fixtures/_domain.py0000644000175000017500000001552612656121545022726 0ustar sayounsayoun00000000000000from datetime import datetime try: # python3 from xmlrpc.client import DateTime except ImportError: # python2 from xmlrpclib import DateTime type_list = list def list(options): return [{'authinfo': 'abcdef0001', 'autorenew': None, 'zone_id': 424242, 'tags': 'bla', 'contacts': {'owner': {'handle': 'AA1-GANDI'}, 'admin': {'handle': 'AA2-GANDI'}, 'bill': {'handle': 'AA3-GANDI'}, 'reseller': {'handle': 'AA4-GANDI'}, 'tech': {'handle': 'AA5-GANDI'}}, 'date_created': datetime(2010, 9, 22, 15, 6, 18), 'date_delete': datetime(2015, 10, 19, 19, 14, 0), 'date_hold_begin': datetime(2015, 9, 22, 22, 0, 0), 'date_registry_creation': datetime(2010, 9, 22, 13, 6, 16), 'date_registry_end': datetime(2015, 9, 22, 0, 0, 0), 'date_updated': datetime(2014, 9, 21, 3, 10, 7), 'nameservers': ['a.dns.gandi.net', 'b.dns.gandi.net', 'c.dns.gandi.net'], 'services': ['gandidns'], 'fqdn': 'iheartcli.com', 'id': 236816922, 'status': [], 'tld': 'com'}, {'authinfo': 'abcdef0002', 'autorenew': None, 'contacts': {'admin': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'bill': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'owner': {'handle': 'PXP561-GANDI', 'id': 2920674}, 'reseller': None, 'tech': {'handle': 'PXP561-GANDI', 'id': 2920674}}, 'date_created': DateTime('20130410T12:46:05'), 'date_delete': DateTime('20160507T07:14:00'), 'date_hold_begin': DateTime('20160410T00:00:00'), 'date_registry_creation': DateTime('20140410T10:46:04'), 'date_registry_end': DateTime('20140410T00:00:00'), 'date_updated': DateTime('20150313T10:30:05'), 'date_hold_end': DateTime('20151020T20:00:00'), 'date_pending_delete_end': DateTime('20151119T00:00:00'), 'date_renew_begin': DateTime('20120101T00:00:00'), 'date_restore_end': DateTime('20151119T00:00:00'), 'fqdn': 'cli.sexy', 'id': 3412062241, 'nameservers': ['a.dns.gandi.net', 'b.dns.gandi.net', 'c.dns.gandi.net'], 'services': ['gandidns', 'gandimail', 'paas'], 'status': [], 'tags': [], 'tld': 'sexy', 'zone_id': None}] def info(id): domain = dict([(domain['fqdn'], domain) for domain in list({})]) return domain[id] def available(domains): ret = {} for domain in domains: if 'unavailable' in domain: ret[domain] = 'unavailable' elif 'pending' in domain: ret[domain] = 'pending' else: ret[domain] = 'available' return ret def create(domain, params): return {'id': 400, 'step': 'WAIT'} def renew(domain, params): return {'id': 400, 'step': 'WAIT'} def mailbox_list(domain, options): return [{'login': 'admin', 'responder': {'active': False}, 'quota': {'granted': 0, 'used': 233}}] def mailbox_info(domain, login): ret = {'aliases': [], 'login': 'admin', 'responder': {'active': False, 'text': None}, 'fallback_email': '', 'quota': {'granted': 0, 'used': 233}} return ret def mailbox_create(domain, login, params): return {'id': 400, 'step': 'WAIT'} def mailbox_delete(domain, login): return {'id': 400, 'step': 'WAIT'} def mailbox_update(domain, login, params): return {'id': 400, 'step': 'WAIT'} def mailbox_alias_set(domain, login, aliases): return {'id': 400, 'step': 'WAIT'} def mailbox_purge(domain, login): return {'id': 400, 'step': 'WAIT'} def forward_list(domain, options): return [{'source': 'admin', 'destinations': ['admin@cli.sexy', 'grumpy@cat.lol']}, {'source': 'contact', 'destinations': ['contact@cli.sexy']}] def forward_create(domain, source, options): return [{'source': source, 'destinations': options['destinations']}] def forward_update(domain, source, options): return [{'source': source, 'destinations': options['destinations']}] def forward_delete(domain, source): return True def zone_record_list(zone_id, version, options=None): ret = [{'id': 337085079, 'name': '*', 'ttl': 10800, 'type': 'A', 'value': '73.246.104.110'}, {'id': 337085078, 'name': '@', 'ttl': 10800, 'type': 'A', 'value': '73.246.104.110'}, {'id': 337085081, 'name': 'much', 'ttl': 10800, 'type': 'A', 'value': '192.243.24.132'}, {'id': 337085072, 'name': 'blog', 'ttl': 10800, 'type': 'CNAME', 'value': 'blogs.vip.gandi.net.'}, {'id': 337085082, 'name': 'cloud', 'ttl': 10800, 'type': 'CNAME', 'value': 'gpaas6.dc0.gandi.net.'}, {'id': 337085075, 'name': 'imap', 'ttl': 10800, 'type': 'CNAME', 'value': 'access.mail.gandi.net.'}, {'id': 337085071, 'name': 'pop', 'ttl': 10800, 'type': 'CNAME', 'value': 'access.mail.gandi.net.'}, {'id': 337085074, 'name': 'smtp', 'ttl': 10800, 'type': 'CNAME', 'value': 'relay.mail.gandi.net.'}, {'id': 337085073, 'name': 'webmail', 'ttl': 10800, 'type': 'CNAME', 'value': 'agent.mail.gandi.net.'}, {'id': 337085077, 'name': '@', 'ttl': 10800, 'type': 'MX', 'value': '50 fb.mail.gandi.net.'}, {'id': 337085076, 'name': '@', 'ttl': 10800, 'type': 'MX', 'value': '10 spool.mail.gandi.net.'}] options = options or {} options.pop('items_per_page', None) def match(zone, options): for fkey in options: if zone[fkey] != options[fkey]: return return zone ret = [zone for zone in ret if match(zone, options)] return ret def zone_version_new(zone_id): return 242424 def zone_record_add(zone_id, version, data): return def zone_version_set(zone_id, version): return def zone_record_delete(zone_id, version, data): return def zone_record_update(zone_id, version, opts, data): return def zone_record_set(zone_id, version, data): return gandi.cli-1.2/gandi/cli/tests/fixtures/_contact.py0000644000175000017500000000325112746404125023100 0ustar sayounsayoun00000000000000 def list(options): return [{'handle': 'AA1-GANDI'}, {'handle': 'AA2-GANDI'}, {'handle': 'AA3-GANDI'}, {'handle': 'AA4-GANDI'}, {'handle': 'AA5-GANDI'}, {'handle': 'TEST1-GANDI'}, {'handle': 'PXP561-GANDI', 'id': 2920674, 'prepaid': {'amount': '1337.42', 'currency': 'EUR'}} ] def info(id='PXP561-GANDI'): contact = dict([(contact['handle'], contact) for contact in list({})]) return contact[id] def create(params): return {'handle': 'PP0000-GANDI'} def create_dry_run(params): errors = [] if params['phone'] == '555-123-456': errors.append({'attr': None, 'error': '!EC_STRMATCH', 'field': 'phone', 'field_type': 'String', 'reason': "phone: string '555-123-456' does not " "match '^\\+\\d{1,3}\\.\\d+$'"}) if params['email'] == 'green.goblin@spiderman.org': # add an unknown error errors.append({'attr': ['Sun, Mercury, Venus, Earth, Mars, Jupiter,' 'Saturn, Uranus, Neptune'], 'error': '!EC_ENUMIN', 'field': 'planet', 'field_type': 'Enum', 'reason': 'planet: Pluto not in list Sun, Mercury, ' 'Venus, Earth, Mars, Jupiter, ' 'Saturn, Uranus, Neptune'}) return errors def balance(id='PXP561-GANDI'): contact = dict([(contact['handle'], contact) for contact in list({})]) return contact[id] gandi.cli-1.2/gandi/cli/tests/fixtures/__init__.py0000644000175000017500000000000012453203306023023 0ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/tests/fixtures/_version.py0000644000175000017500000000006312507007717023131 0ustar sayounsayoun00000000000000 def info(): return {'api_version': '3.3.42'} gandi.cli-1.2/gandi/cli/tests/fixtures/_operation.py0000644000175000017500000001323213164644453023452 0ustar sayounsayoun00000000000000try: # python3 from xmlrpc.client import DateTime except ImportError: # python2 from xmlrpclib import DateTime type_list = list def list(options): ret = [{'date_created': DateTime('20150915T18:29:16'), 'date_start': None, 'date_updated': DateTime('20150915T18:29:17'), 'errortype': None, 'eta': -1863666, 'id': 100100, 'cert_id': None, 'infos': {'extras': {}, 'id': '', 'label': 'iheartcli.com', 'product_action': 'renew', 'product_name': 'com', 'product_type': 'domain', 'quantity': ''}, 'last_error': None, 'params': {'auth_id': 99999999, 'current_year': 2015, 'domain': 'iheartcli.com', 'domain_id': 1234567, 'duration': 1, 'param_type': 'domain', 'remote_addr': '127.0.0.1', 'session_id': 2920674, 'tld': 'com', 'tracker_id': '621cb9f4-472d-4cc1-b4b9-b18cc61e2914'}, 'session_id': 2920674, 'source': 'PXP561-GANDI', 'step': 'BILL', 'type': 'domain_renew'}, {'date_created': DateTime('20150505T00:00:00'), 'date_start': None, 'date_updated': DateTime('20150505T00:00:00'), 'errortype': None, 'eta': 0, 'id': 100200, 'cert_id': None, 'infos': {'extras': {}, 'id': '', 'label': '', 'product_action': 'billing_prepaid_add_money', 'product_name': '', 'product_type': 'corporate', 'quantity': ''}, 'last_error': None, 'params': {'amount': 50.0, 'auth_id': 99999999, 'param_type': 'prepaid_add_money', 'prepaid_id': 100000, 'remote_addr': '127.0.0.1', 'tracker_id': 'ab0e5e67-6ca7-4afc-8311-f20080f15cf1'}, 'session_id': 9844958, 'source': 'PXP561-GANDI', 'step': 'BILL', 'type': 'billing_prepaid_add_money'}, {'step': 'RUN', 'cert_id': 710, 'id': 100300, 'type': 'certificate_update', 'params': {'cert_id': 710, 'param_type': 'certificate_update', 'prepaid_id': 100000, 'inner_step': 'comodo_oper_updated', 'dcv_method': 'email', 'csr': '-----BEGIN CERTIFICATE REQUEST-----' 'MIICxjCCAa4CAQAwgYAxCzAJBgNVBAYTAkZSMQsw' '0eWfyJJTOypoToCtdGoye507GOsgIysfRWaExay5' '-----END CERTIFICATE REQUEST-----', 'remote_addr': '127.0.0.1'}}, {'step': 'RUN', 'cert_id': 706, 'id': 100302, 'type': 'certificate_update', 'params': {'cert_id': 706, 'param_type': 'certificate_update', 'prepaid_id': 100000, 'inner_step': 'comodo_oper_updated', 'dcv_method': 'dns', 'csr': '-----BEGIN CERTIFICATE REQUEST-----' 'MIICxjCCAa4CAQAwgYAxCzAJBgNVBAYTAkZSMQsw' '0eWfyJJTOypoToCtdGoye507GOsgIysfRWaExay5' '-----END CERTIFICATE REQUEST-----', 'remote_addr': '127.0.0.1'}}, {'step': 'WAIT', 'cert_id': 701, 'id': 100303, 'type': 'certificate_update', 'params': {'cert_id': 706, 'param_type': 'certificate_update', 'prepaid_id': 100000, 'inner_step': 'check_email_sent', 'dcv_method': 'dns', 'remote_addr': '127.0.0.1'}}, {'step': 'RUN', 'id': 99001, 'vm_id': 152967, 'type': 'hosting_migration_vm', 'params': {'inner_step': 'wait_sync'}}, {'step': 'RUN', 'id': 99002, 'vm_id': 152966, 'type': 'hosting_migration_vm', 'params': {'inner_step': 'wait_finalize'}}, ] options.pop('sort_by', None) options.pop('items_per_page', None) def compare(op, option): if isinstance(option, (type_list, tuple)): return op in option return op == option for fkey in options: ret = [op for op in ret if compare(op.get(fkey), options[fkey])] return ret def info(id): if id == 200: return {'step': 'DONE'} if id == 300: return {'step': 'DONE', 'vm_id': 9000} if id == 400: return {'step': 'DONE'} if id == 600: return {'step': 'DONE', 'type': 'certificate_update', 'params': {'cert_id': 710, 'param_type': 'certificate_update', 'prepaid_id': 100000, 'remote_addr': '127.0.0.1'}} if id == 9900: return {'step': 'DONE', 'type': 'hosting_migration_disk', 'params': {'to_dc_id': 3, 'from_dc_id': 1, 'inner_step': 'wait_finalize'}, 'id': 9900} return [oper for oper in list({}) if oper['id'] == id][0] gandi.cli-1.2/gandi/cli/tests/compat.py0000644000175000017500000000071713227142762020725 0ustar sayounsayoun00000000000000 try: from unittest import mock except ImportError: import mock try: import unittest2 as unittest except ImportError: import unittest try: import configparser as ConfigParser except ImportError: import ConfigParser try: from StringIO import StringIO except ImportError: from io import StringIO try: from cStringIO import StringIO as ReasonableBytesIO except ImportError: from io import BytesIO as ReasonableBytesIO gandi.cli-1.2/gandi/cli/tests/test_main.py0000644000175000017500000000047212501555263021421 0ustar sayounsayoun00000000000000from .compat import unittest from .compat import mock class TestCase(unittest.TestCase): def test_main(self): cli = mock.Mock() with mock.patch('gandi.cli.core.cli.cli', cli): from gandi.cli.__main__ import main main() cli.assert_called_once_with(obj={}) gandi.cli-1.2/gandi/cli/core/0000755000175000017500000000000013227415174016651 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/core/cli.py0000644000175000017500000001243513227374032017774 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- """ GandiCLI class declaration and initialization. """ import os import os.path import inspect import platform from functools import update_wrapper import click from .base import GandiContextHelper from gandi.cli import __version__ try: use_man_epilog = platform.system() == 'Linux' except: pass # XXX: dirty hack of click help command to allow short help -h def add_help_option(self): """Add a help option to the command.""" click.help_option(*('--help', '-h'))(self) click.Command.add_help_option = add_help_option # XXX: patch each command with an epilog if use_man_epilog: def format_epilog(self, ctx, formatter): """Writes the epilog into the formatter if it exists.""" self.epilog = 'For detailed documentation, use `man gandi`.' formatter.write_paragraph() with formatter.indentation(): formatter.write_text(self.epilog) click.Command.format_epilog = format_epilog def compatcallback(f): """ Compatibility callback decorator for older click version. Click 1.0 does not have a version string stored, so we need to use getattr here to be safe. """ if getattr(click, '__version__', '0.0') >= '2.0': return f return update_wrapper(lambda ctx, value: f(ctx, None, value), f) class GandiCLI(click.Group): """ Gandi command line utility. All CLI commands have a documented help $ gandi --help """ def __init__(self, help=None): """ Initialize CLI command line.""" @compatcallback def set_debug(ctx, param, value): ctx.obj['verbose'] = value @compatcallback def get_version(ctx, param, value): if value: print(('Gandi CLI %s\n\n' 'Copyright: © 2014-2018 Gandi S.A.S.\n' 'License: GPL-3' % __version__)) ctx.exit() if help is None: help = inspect.getdoc(self) click.Group.__init__(self, help=help, params=[ click.Option(['-v'], help='Enable or disable verbose mode. Use multiple ' 'time for higher level of verbosity: -v, -vv', count=True, metavar='', default=0, callback=set_debug), click.Option(['--version'], help='Display version.', is_flag=True, default=False, callback=get_version) ]) def format_commands(self, ctx, formatter): """Extra format methods for multi methods that adds all the commands after the options. Display custom help for all subcommands. """ rows = [] all_cmds = self.list_all_commands(ctx) for cmd_name in sorted(all_cmds): cmd = all_cmds[cmd_name] help = cmd.short_help or '' rows.append((cmd_name, help)) if rows: with formatter.section('Commands'): formatter.write_dl(rows) def list_sub_commmands(self, cmd_name, cmd): """Return all commands for a group""" ret = {} if isinstance(cmd, click.core.Group): for sub_cmd_name in cmd.commands: sub_cmd = cmd.commands[sub_cmd_name] sub = self.list_sub_commmands(sub_cmd_name, sub_cmd) if sub: if isinstance(sub, dict): for n, c in sub.items(): ret['%s %s' % (cmd_name, n)] = c else: ret['%s %s' % (cmd_name, sub[0])] = sub[1] elif isinstance(cmd, click.core.Command): return (cmd.name, cmd) return ret def list_all_commands(self, ctx): ret = {} for cmd_name in self.commands: cmd = self.commands[cmd_name] sub = self.list_sub_commmands(cmd_name, cmd) if sub: if isinstance(sub, tuple): ret[sub[0]] = sub[1] else: ret.update(sub) return ret def load_commands(self): """ Load cli commands from submodules. """ command_folder = os.path.join(os.path.dirname(__file__), '..', 'commands') command_dirs = { 'gandi.cli': command_folder } if 'GANDICLI_PATH' in os.environ: for _path in os.environ.get('GANDICLI_PATH').split(':'): # remove trailing separator if any path = _path.rstrip(os.sep) command_dirs[os.path.basename(path)] = os.path.join(path, 'commands') for module_basename, dir in list(command_dirs.items()): for filename in sorted(os.listdir(dir)): if filename.endswith('.py') and '__init__' not in filename: submod = filename[:-3] module_name = module_basename + '.commands.' + submod __import__(module_name, fromlist=[module_name]) def invoke(self, ctx): """ Invoke command in context. """ ctx.obj = GandiContextHelper(verbose=ctx.obj['verbose']) click.Group.invoke(self, ctx) cli = GandiCLI() cli.load_commands() gandi.cli-1.2/gandi/cli/core/base.py0000644000175000017500000003213413227142736020141 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- """ Contains GandiModule and GandiContextHelper classes. GandiModule class is used by commands modules. GandiContextHelper class is used as click context for commands. """ from __future__ import print_function import os import sys import time import os.path from datetime import datetime from subprocess import check_call, Popen, PIPE, CalledProcessError import click from click.exceptions import UsageError from .client import XMLRPCClient, APICallFailed, DryRunException, JsonClient from .conf import GandiConfig class MissingConfiguration(Exception): """ Raise when no configuration was found. """ class GandiModule(GandiConfig): """ Base class for modules. Manage - initializing xmlrpc connection - execute remote api calls """ _op_scores = {'BILL': 0, 'WAIT': 1, 'RUN': 2, 'DONE': 3} verbose = 0 _api = None # frequency of api calls when polling for operation progress _poll_freq = 1 @classmethod def get_api_connector(cls): """ Initialize an api connector for future use.""" if cls._api is None: # pragma: no cover cls.load_config() cls.debug('initialize connection to remote server') apihost = cls.get('api.host') if not apihost: raise MissingConfiguration() apienv = cls.get('api.env') if apienv and apienv in cls.apienvs: apihost = cls.apienvs[apienv] cls._api = XMLRPCClient(host=apihost, debug=cls.verbose) return cls._api @classmethod def call(cls, method, *args, **kwargs): """ Call a remote api method and return the result.""" api = None empty_key = kwargs.pop('empty_key', False) try: api = cls.get_api_connector() apikey = cls.get('api.key') if not apikey and not empty_key: cls.echo("No apikey found, please use 'gandi setup' " "command") sys.exit(1) except MissingConfiguration: if api and empty_key: apikey = '' elif not kwargs.get('safe'): cls.echo("No configuration found, please use 'gandi setup' " "command") sys.exit(1) else: return [] # make the call cls.debug('calling method: %s' % method) for arg in args: cls.debug('with params: %r' % arg) try: resp = api.request(method, apikey, *args, **{'dry_run': kwargs.get('dry_run', False), 'return_dry_run': kwargs.get('return_dry_run', False)}) cls.dump('responded: %r' % resp) return resp except APICallFailed as err: if kwargs.get('safe'): return [] if err.code == 530040: cls.echo("Error: It appears you haven't purchased any credits " "yet.\n" "Please visit https://www.gandi.net/credit/buy to " "learn more and buy credits.") sys.exit(1) if err.code == 510150: cls.echo("Invalid API key, please use 'gandi setup' command.") sys.exit(1) if isinstance(err, DryRunException): if kwargs.get('return_dry_run', False): return err.dry_run else: for msg in err.dry_run: # TODO use trads with %s cls.echo(msg['reason']) cls.echo('\t' + ' '.join(msg['attr'])) sys.exit(1) error = UsageError(err.errors) setattr(error, 'code', err.code) raise error @classmethod def safe_call(cls, method, *args): """ Call a remote api method but don't raise if an error occurred.""" return cls.call(method, *args, safe=True) @classmethod def json_call(cls, method, url, **kwargs): """ Call a remote api using json format """ # retrieve api key if needed empty_key = kwargs.pop('empty_key', False) send_key = kwargs.pop('send_key', True) return_header = kwargs.pop('return_header', False) try: apikey = cls.get('apirest.key') if not apikey and not empty_key: cls.echo("No apikey found for REST API, please use " "'gandi setup' command") sys.exit(1) if send_key: if 'headers' in kwargs: kwargs['headers'].update({'X-Api-Key': apikey}) else: kwargs['headers'] = {'X-Api-Key': apikey} except MissingConfiguration: if not empty_key: return [] # make the call cls.debug('calling url: %s %s' % (method, url)) cls.debug('with params: %r' % kwargs) try: resp, resp_headers = JsonClient.request(method, url, **kwargs) cls.dump('responded: %r' % resp) if return_header: return resp, resp_headers return resp except APICallFailed as err: cls.echo('An error occured during call: %s' % err.errors) sys.exit(1) @classmethod def json_get(cls, url, **kwargs): """ Helper for GET json request """ return cls.json_call('GET', url, **kwargs) @classmethod def json_post(cls, url, **kwargs): """ Helper for POST json request """ return cls.json_call('POST', url, **kwargs) @classmethod def json_put(cls, url, **kwargs): """ Helper for PUT json request """ return cls.json_call('PUT', url, **kwargs) @classmethod def json_delete(cls, url, **kwargs): """ Helper for DELETE json request """ return cls.json_call('DELETE', url, **kwargs) @classmethod def intty(cls): """ Check if we are in a tty. """ # XXX: temporary hack until we can detect if we are in a pipe or not return True if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): return True return False @classmethod def echo(cls, message): """ Display message. """ if cls.intty(): if message is not None: print(message) @classmethod def pretty_echo(cls, message): """ Display message using pretty print formatting. """ if cls.intty(): if message: from pprint import pprint pprint(message) @classmethod def separator_line(cls, sep='-', size=10): """ Display a separator line. """ if cls.intty(): cls.echo(sep * size) @classmethod def separator_sub_line(cls, sep='-', size=10): """ Display a separator line. """ if cls.intty(): cls.echo("\t" + sep * size) @classmethod def dump(cls, message): """ Display dump message if verbose level allows it. """ if cls.verbose > 2: msg = '[DUMP] %s' % message cls.echo(msg) @classmethod def debug(cls, message): """ Display debug message if verbose level allows it. """ if cls.verbose > 1: msg = '[DEBUG] %s' % message cls.echo(msg) @classmethod def log(cls, message): """ Display info message if verbose level allows it. """ if cls.verbose > 0: msg = '[INFO] %s' % message cls.echo(msg) @classmethod def deprecated(cls, message): print('[deprecated]: %s' % message, file=sys.stderr) @classmethod def error(cls, msg): """ Raise click UsageError exception using msg. """ raise UsageError(msg) @classmethod def execute(cls, command, shell=True): """ Execute a shell command. """ cls.debug('execute command (shell flag:%r): %r ' % (shell, command)) try: check_call(command, shell=shell) return True except CalledProcessError: return False @classmethod def exec_output(cls, command, shell=True, encoding='utf-8'): """ Return execution output :param encoding: charset used to decode the stdout :type encoding: str :return: the return of the command :rtype: unicode string """ proc = Popen(command, shell=shell, stdout=PIPE) stdout, _stderr = proc.communicate() if proc.returncode == 0: return stdout.decode(encoding) return '' @classmethod def update_progress(cls, progress, starttime): """ Display an ascii progress bar while processing operation. """ width, _height = click.get_terminal_size() if not width: return duration = datetime.utcnow() - starttime hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) size = int(width * .6) status = "" if isinstance(progress, int): progress = float(progress) if not isinstance(progress, float): progress = 0 status = 'error: progress var must be float\n' cls.echo(type(progress)) if progress < 0: progress = 0 status = 'Halt...\n' if progress >= 1: progress = 1 # status = 'Done...\n' block = int(round(size * progress)) text = ('\rProgress: [{0}] {1:.2%} {2} {3:0>2}:{4:0>2}:{5:0>2} ' ''.format('#' * block + '-' * (size - block), progress, status, hours, minutes, seconds)) sys.stdout.write(text) sys.stdout.flush() @classmethod def display_progress(cls, operations): """ Display progress of Gandi operations. polls API every 1 seconds to retrieve status. """ start_crea = datetime.utcnow() # count number of operations, 3 steps per operation if not isinstance(operations, (list, tuple)): operations = [operations] count_operations = len(operations) * 3 updating_done = False while not updating_done: op_score = 0 for oper in operations: op_ret = cls.call('operation.info', oper['id']) op_step = op_ret['step'] if op_step in cls._op_scores: op_score += cls._op_scores[op_step] else: cls.echo('') msg = 'step %s unknown, exiting' % op_step if op_step == 'ERROR': msg = ('An error has occured during operation ' 'processing: %s' % op_ret['last_error']) elif op_step == 'SUPPORT': msg = ('An error has occured during operation ' 'processing, you must contact Gandi support.') cls.echo(msg) sys.exit(1) cls.update_progress(float(op_score) / count_operations, start_crea) if op_score == count_operations: updating_done = True time.sleep(cls._poll_freq) cls.echo('\r') class GandiContextHelper(GandiModule): """ Gandi context helper. Import module classes from modules directory upon initialization. """ _modules = {} def __init__(self, verbose=-1): """ Initialize variables and api connection. """ GandiModule.verbose = verbose GandiModule.load_config() # only load modules once if not self._modules: self.load_modules() def __getattribute__(self, item): """ Return module from internal imported modules dict. """ if item in object.__getattribute__(self, '_modules'): return object.__getattribute__(self, '_modules')[item] return object.__getattribute__(self, item) def load_modules(self): """ Import CLI commands modules. """ module_folder = os.path.join(os.path.dirname(__file__), '..', 'modules') module_dirs = { 'gandi.cli': module_folder } if 'GANDICLI_PATH' in os.environ: for _path in os.environ.get('GANDICLI_PATH').split(':'): # remove trailing separator if any path = _path.rstrip(os.sep) module_dirs[os.path.basename(path)] = os.path.join(path, 'modules') for module_basename, dir in list(module_dirs.items()): for filename in sorted(os.listdir(dir)): if filename.endswith('.py') and '__init__' not in filename: submod = filename[:-3] module_name = module_basename + '.modules.' + submod __import__(module_name, fromlist=[module_name]) # save internal map of loaded module classes for subclass in GandiModule.__subclasses__(): self._modules[subclass.__name__.lower()] = subclass gandi.cli-1.2/gandi/cli/core/utils/0000755000175000017500000000000013227415174020011 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/core/utils/unixpipe.py0000644000175000017500000001361712764262127022237 0ustar sayounsayoun00000000000000#!/usr/bin/python import os import socket import select import subprocess import sys import time class FdPipe: """Connect two pairs of file objects""" def __init__(self, in0, out0, in1, out1): self.poller = select.poll() self.fd_map = { out0.fileno(): in1.fileno(), out1.fileno(): in0.fileno() } for fd in self.fd_map: self.select_for_read(fd) self.out_buf = {} def select_for_flush(self, fd): self.poller.register(fd, select.POLLERR | select.POLLHUP | select.POLLIN | select.POLLOUT) def select_for_write(self, fd): self.poller.register(fd, select.POLLERR | select.POLLHUP | select.POLLIN | select.POLLOUT) def select_for_read(self, fd): self.poller.register(fd, select.POLLERR | select.POLLHUP | select.POLLIN) def do_write(self, outfd): if not self.out_buf[outfd]: return (rl, wl, el) = select.select([], [outfd], [outfd]) if outfd in wl: length = os.write(outfd, self.out_buf[outfd]) self.out_buf[outfd] = self.out_buf[outfd][length:] if not self.out_buf[outfd]: self.select_for_read(outfd) del self.out_buf[outfd] elif outfd in el: raise Exception('could not flush fd') def queue_write(self, outfd, data): self.out_buf[outfd] = self.out_buf.setdefault(outfd, '') + data self.select_for_write(outfd) def flush_outputs(self): while self.out_buf: for outfd in self.out_buf: try: self.do_write(outfd) except OSError: return def one_loop(self): ret = True for (fd, ev) in self.poller.poll(1000): if ev & (select.POLLERR | select.POLLHUP): self.flush_outputs() self.poller.unregister(fd) ret = False if ev & select.POLLIN: data = os.read(fd, 4096) if not data: os.close(self.fd_map[fd]) ret = False self.queue_write(self.fd_map[fd], data) if ev & select.POLLOUT: self.do_write(fd) return ret def scp(addr, user, local_path, remote_path, local_key=None): scp_call = ['scp', local_path, '%s@[%s]:%s' % (user, addr, remote_path)] if local_key: scp_call.insert(1, local_key) scp_call.insert(1, '-i') subprocess.call(scp_call) def tcp4_to_unix(local_port, unix_path): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: server.bind(('127.0.0.1', local_port)) except socket.error as e: sys.stderr.write('remote cant grab port %d\n' % local_port) # let other end time to connect to maintain ssh up time.sleep(10) sys.exit(0) server.listen(32) while True: (rl, wl, el) = select.select([server], [], [server], 1) if server in rl: (client, _) = server.accept() if not os.fork(): unix = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: unix.connect(unix_path) except socket.error as e: print('Unable to grab %s: %s' % (unix_path, e)) pipe = FdPipe(client, client, unix, unix) while pipe.one_loop(): pass return client.close() try: os.waitpid(-1, os.WNOHANG) except OSError: pass def find_port(addr, user): """Find local port in existing tunnels""" import pwd home = pwd.getpwuid(os.getuid()).pw_dir for name in os.listdir('%s/.ssh/' % home): if name.startswith('unixpipe_%s@%s_' % (user, addr,)): return int(name.split('_')[2]) def new_port(): """Find a free local port and allocate it""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) for i in range(12042, 16042): try: s.bind(('127.0.0.1', i)) s.close() return i except socket.error: pass raise Exception('No local port available') def _ssh_master_cmd(addr, user, command, local_key=None): """Exit or check ssh mux""" ssh_call = ['ssh', '-qNfL%d:127.0.0.1:12042' % find_port(addr, user), '-o', 'ControlPath=~/.ssh/unixpipe_%%r@%%h_%d' % find_port(addr, user), '-O', command, '%s@%s' % (user, addr,)] if local_key: ssh_call.insert(1, local_key) ssh_call.insert(1, '-i') return subprocess.call(ssh_call) def is_alive(addr, user): """Check wether a tunnel is alive""" return _ssh_master_cmd(addr, user, 'check') == 0 def setup(addr, user, remote_path, local_key=None): """Setup the tunnel""" port = find_port(addr, user) if not port or not is_alive(addr, user): port = new_port() scp(addr, user, __file__, '~/unixpipe', local_key) ssh_call = ['ssh', '-fL%d:127.0.0.1:12042' % port, '-o', 'ExitOnForwardFailure=yes', '-o', 'ControlPath=~/.ssh/unixpipe_%%r@%%h_%d' % port, '-o', 'ControlMaster=auto', '%s@%s' % (user, addr,), 'python', '~/unixpipe', 'server', remote_path] if local_key: ssh_call.insert(1, local_key) ssh_call.insert(1, '-i') subprocess.call(ssh_call) # XXX Sleep is a bad way to wait for the tunnel endpoint time.sleep(1) return port if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == 'server': tcp4_to_unix(12042, sys.argv[2]) gandi.cli-1.2/gandi/cli/core/utils/xmlrpc.py0000644000175000017500000000403012506727670021673 0ustar sayounsayoun00000000000000""" Security enhancements for xmlrpc. """ from __future__ import print_function import sys try: import xmlrpc.client as xmlrpclib except ImportError: import xmlrpclib try: import requests except ImportError: print('python requests is required, please reinstall.', file=sys.stderr) sys.exit(1) class RequestsTransport(xmlrpclib.Transport): """ Drop in Transport for xmlrpclib that uses Requests instead of httplib # https://gist.github.com/chrisguitarguy/2354951 # https://github.com/mardiros/pyshop/blob/master/pyshop/helpers/pypi.py """ use_https = True def __init__(self, use_datetime=0, host=None): xmlrpclib.Transport.__init__(self, use_datetime) if host: self.use_https = 'https' in host def request(self, host, handler, request_body, verbose): """ Make an xmlrpc request. """ headers = {'User-Agent': self.user_agent, 'Accept': 'text/xml', 'Content-Type': 'text/xml'} url = self._build_url(host, handler) try: resp = requests.post(url, data=request_body, headers=headers) except ValueError: raise except Exception: raise # something went wrong else: try: resp.raise_for_status() except requests.RequestException as e: raise xmlrpclib.ProtocolError(url, resp.status_code, str(e), resp.headers) else: return self.parse_response(resp) def parse_response(self, resp): """ Parse the xmlrpc response. """ p, u = self.getparser() p.feed(resp.content) p.close() return u.close() def _build_url(self, host, handler): """ Build a url for our request based on the host, handler and use_https property """ scheme = 'https' if self.use_https else 'http' return '%s://%s%s' % (scheme, host, handler) gandi.cli-1.2/gandi/cli/core/utils/password.py0000644000175000017500000000231613227414340022221 0ustar sayounsayoun00000000000000# coding: utf-8 """Contains methods to generate a random password.""" import random import string # remove backslash from generated password to avoid # general escape issues while transmitting it PUNCTUATION = string.punctuation.replace(chr(0x5c), '') def mkpassword(length=16, chars=None, punctuation=None): """Generates a random ascii string - useful to generate authinfos :param length: string wanted length :type length: ``int`` :param chars: character population, defaults to alphabet (lower & upper) + numbers :type chars: ``str``, ``list``, ``set`` (sequence) :param punctuation: number of punctuation signs to include in string :type punctuation: ``int`` :rtype: ``str`` """ if chars is None: chars = string.ascii_letters + string.digits # Generate string from population data = [random.choice(chars) for _ in range(length)] # If punctuation: # - remove n chars from string # - add random punctuation # - shuffle chars :) if punctuation: data = data[:-punctuation] for _ in range(punctuation): data.append(random.choice(PUNCTUATION)) random.shuffle(data) return ''.join(data) gandi.cli-1.2/gandi/cli/core/utils/__init__.py0000644000175000017500000004571713164644614022143 0ustar sayounsayoun00000000000000""" Contains output methods used by commands. Also custom exceptions and method to generate a random string. """ import sys import time from datetime import datetime import json from click.formatting import measure_table from click import ClickException from .ascii_sparks import sparks class MissingConfiguration(Exception): """ Raise when configuration if missing. """ def __init__(self, errors): """ Initialize exception.""" self.errors = errors class DuplicateResults(Exception): """ Raise when multiple results are found.""" def __init__(self, errors): """ Initialize exception.""" self.errors = errors class DomainNotAvailable(Exception): """ Raise when domain is not available. """ def __init__(self, errors): """ Initialize exception.""" self.errors = errors class DatacenterClosed(ClickException): """Raise when datacenter is closed: ALL""" def __init__(self, message): """ Initialize exception.""" self.message = message class DatacenterLimited(Exception): """Raise when datacenter will soon be closed: NEW""" def __init__(self, date): """ Initialize exception.""" self.date = date class MigrationNotFinalized(ClickException): """Raise when VM migration does not need to be finalized.""" def __init__(self, message): """ Initialize exception.""" self.message = message def format_list(data): """ Remove useless characters to output a clean list.""" if isinstance(data, (list, tuple)): to_clean = ['[', ']', '(', ')', "'"] for item in to_clean: data = str(data).replace("u\"", "\"").replace("u\'", "\'") data = str(data).replace(item, '') return data def display_rows(gandi, rows, has_header=True): col_len = measure_table(rows) formatting = ' | '.join(['%-' + str(l) + 's' for l in col_len]) if has_header: header = rows.pop(0) gandi.echo(formatting % tuple(header)) gandi.echo('-+-'.join(['-' * l for l in col_len])) for row in rows: gandi.echo(formatting % tuple(row)) def output_line(gandi, key, val, justify): """ Base helper to output a key value using left justify.""" msg = ('%%-%ds:%%s' % justify) % (key, (' %s' % val) if val else '') gandi.echo(msg) def output_generic(gandi, data, output_keys, justify=10): """ Generic helper to output info from a data dict.""" for key in output_keys: if key in data: output_line(gandi, key, data[key], justify) def output_account(gandi, account, output_keys, justify=17): """ Helper to output an account information.""" output_generic(gandi, account, output_keys, justify) if 'prepaid' in output_keys: prepaid = '%s %s' % (account['prepaid_info']['amount'], account['prepaid_info']['currency']) output_line(gandi, 'prepaid', prepaid, justify) if 'credit' in output_keys: output_line(gandi, 'credits', None, justify) available = account.get('credits') output_line(gandi, ' available', available, justify) # sometimes rating is returning nothing usage_str = left_str = 'not available' usage = account.get('credit_usage', 0) left = account.get('left') if usage: usage_str = '%d/h' % usage years, months, days, hours = left left_str = ('%d year(s) %d month(s) %d day(s) %d hour(s)' % (years, months, days, hours)) output_line(gandi, ' usage', usage_str, justify) output_line(gandi, ' time left', left_str, justify) def output_vm(gandi, vm, datacenters, output_keys, justify=10): """ Helper to output a vm information.""" output_generic(gandi, vm, output_keys, justify) if 'datacenter' in output_keys: for dc in datacenters: if dc['id'] == vm['datacenter_id']: dc_name = dc.get('dc_code', dc.get('iso', '')) break output_line(gandi, 'datacenter', dc_name, justify) if 'ip' in output_keys: for iface in vm['ifaces']: gandi.separator_line() output_line(gandi, 'bandwidth', iface['bandwidth'], justify) for ip in iface['ips']: ip_addr = ip['ip'] output_line(gandi, 'ip%s' % ip['version'], ip_addr, justify) def output_metric(gandi, metrics, key, justify=10): """ Helper to output metrics.""" for metric in metrics: key_name = metric[key].pop() values = [point.get('value', 0) for point in metric['points']] graph = sparks(values) if max(values) else '' # need to encode in utf-8 to work in python2.X if sys.version_info < (2, 8): graph = graph.encode('utf-8') output_line(gandi, key_name, graph, justify) def output_vhost(gandi, vhost, paas, output_keys, justify=14): """ Helper to output a vhost information.""" output_generic(gandi, vhost, output_keys, justify) if 'paas_name' in output_keys: output_line(gandi, 'paas_name', paas, justify) def output_paas(gandi, paas, datacenters, vhosts, output_keys, justify=11): """ Helper to output a paas information.""" output_generic(gandi, paas, output_keys, justify) if 'sftp_server' in output_keys: output_line(gandi, 'sftp_server', paas['ftp_server'], justify) if 'vhost' in output_keys: for entry in vhosts: output_line(gandi, 'vhost', entry, justify) if 'dc' in output_keys: dc_name = paas['datacenter'].get('dc_code', paas['datacenter'].get('iso', '')) output_line(gandi, 'datacenter', dc_name, justify) if 'df' in paas: df = paas['df'] total = df['free'] + df['used'] if total: disk_used = '%.1f%%' % (df['used'] * 100 / total) output_line(gandi, 'quota used', disk_used, justify) if 'snapshot' in output_keys: val = None if paas['snapshot_profile']: val = paas['snapshot_profile']['name'] output_line(gandi, 'snapshot', val, justify) if 'cache' in paas: cache = paas['cache'] total = cache['hit'] + cache['miss'] + cache['not'] + cache['pass'] if total: output_line(gandi, 'cache', None, justify) for key in sorted(cache): str_value = '%.1f%%' % (cache[key] * 100 / total) output_sub_line(gandi, key, str_value, 5) def output_image(gandi, image, datacenters, output_keys, justify=14, warn_deprecated=True): """ Helper to output a disk image.""" for key in output_keys: if key in image: if (key == 'label' and image['visibility'] == 'deprecated' and warn_deprecated): image[key] = '%s /!\ DEPRECATED' % image[key] output_line(gandi, key, image[key], justify) dc_name = 'Nowhere' if 'dc' in output_keys: for dc in datacenters: if dc['id'] == image['datacenter_id']: dc_name = dc.get('dc_code', dc.get('iso', '')) break output_line(gandi, 'datacenter', dc_name, justify) def output_kernels(gandi, flavor, name_list, justify=14): """ Helper to output kernel flavor versions.""" output_line(gandi, 'flavor', flavor, justify) for name in name_list: output_line(gandi, 'version', name, justify) def output_datacenter(gandi, datacenter, output_keys, justify=14): """ Helper to output datacenter information.""" output_generic(gandi, datacenter, output_keys, justify) if 'dc_name' in output_keys: output_line(gandi, 'datacenter', datacenter['name'], justify) if 'status' in output_keys: deactivate_at = datacenter.get('deactivate_at') if deactivate_at: output_line(gandi, 'closing on', deactivate_at.strftime('%d/%m/%Y'), justify) closing = [] iaas_closed_for = datacenter.get('iaas_closed_for') if iaas_closed_for == 'ALL': closing.append('vm') paas_closed_for = datacenter.get('paas_closed_for') if paas_closed_for == 'ALL': closing.append('paas') if closing: output_line(gandi, 'closed for', ', '.join(closing), justify) def output_cmdline(gandi, cmdline, justify=14): args = [] for key in sorted(cmdline, reverse=True): if isinstance(cmdline[key], bool): args.append(key) else: args.append('%s=%s' % (key, cmdline[key])) output_line(gandi, 'cmdline', ' '.join(args), justify) def output_disk(gandi, disk, datacenters, vms, profiles, output_keys, justify=10): """ Helper to output a disk.""" output_generic(gandi, disk, output_keys, justify) if 'kernel' in output_keys and disk.get('kernel_version'): output_line(gandi, 'kernel', disk['kernel_version'], justify) if 'cmdline' in output_keys and disk.get('kernel_cmdline'): output_cmdline(gandi, disk.get('kernel_cmdline'), justify) if 'dc' in output_keys: dc_name = None for dc in datacenters: if dc['id'] == disk['datacenter_id']: dc_name = dc.get('dc_code', dc.get('iso', '')) break if dc_name: output_line(gandi, 'datacenter', dc_name, justify) if 'vm' in output_keys: for vm_id in disk['vms_id']: vm_name = vms.get(vm_id, {}).get('hostname') if vm_name: output_line(gandi, 'vm', vm_name, justify) if 'profile' in output_keys and disk.get('snapshot_profile'): output_line(gandi, 'profile', disk['snapshot_profile']['name'], justify) elif 'profile' in output_keys and disk.get('snapshot_profile_id'): for profile in profiles: if profile['id'] == disk['snapshot_profile_id']: output_line(gandi, 'profile', profile['name'], justify) break def output_sshkey(gandi, sshkey, output_keys, justify=12): """ Helper to output an ssh key information.""" output_generic(gandi, sshkey, output_keys, justify) def output_snapshot_profile(gandi, profile, output_keys, justify=13): """ Helper to output a snapshot_profile.""" schedules = 'schedules' in output_keys if schedules: output_keys.remove('schedules') output_generic(gandi, profile, output_keys, justify) if schedules: schedule_keys = ['name', 'kept_version'] for schedule in profile['schedules']: gandi.separator_line() output_generic(gandi, schedule, schedule_keys, justify) def output_contact_info(gandi, data, output_keys, justify=10): """Helper to output chosen contacts info.""" for key in output_keys: if data[key]: output_line(gandi, key, data[key]['handle'], justify) def output_cert_oper(gandi, oper, justify=12): output_generic(gandi, oper, ['type', 'step'], justify) params = dict(oper['params']) params['fqdns'] = ', '.join(params.get('fqdns', [])) output = ['inner_step', 'package_name', 'dcv_method'] if params['fqdns']: output.append('fqdns') output_generic(gandi, params, output, justify) def output_cert(gandi, cert, output_keys, justify=13): """Helper to output a certificate information.""" output = list(output_keys) display_altnames = False if 'altnames' in output: display_altnames = True output.remove('altnames') display_output = False if 'cert' in output: display_output = True output.remove('cert') output_generic(gandi, cert, output, justify) if display_output: crt = gandi.certificate.pretty_format_cert(cert) if crt: output_line(gandi, 'cert', '\n' + crt, justify) if display_altnames: for altname in cert['altnames']: output_line(gandi, 'altname', altname, justify) def output_vlan(gandi, vlan, datacenters, output_keys, justify=10): """ Helper to output a vlan information.""" output_generic(gandi, vlan, output_keys, justify) if 'dc' in output_keys: for dc in datacenters: if dc['id'] == vlan.get('datacenter_id', vlan.get('datacenter', {}).get('id')): dc_name = dc.get('dc_code', dc.get('iso', '')) break output_line(gandi, 'datacenter', dc_name, justify) def output_iface(gandi, iface, datacenters, vms, output_keys, justify=10): """ Helper to output an iface information.""" output_generic(gandi, iface, output_keys, justify) if 'vm' in output_keys: vm_name = vms.get(iface['vm_id'], {}).get('hostname') if vm_name: output_line(gandi, 'vm', vm_name, justify) if 'dc' in output_keys: for dc in datacenters: if dc['id'] == iface.get('datacenter_id', iface.get('datacenter', {}).get('id')): dc_name = dc.get('dc_code', dc.get('iso', '')) break output_line(gandi, 'datacenter', dc_name, justify) if 'vlan_' in output_keys: vlan = iface.get('vlan') or {} output_line(gandi, 'vlan', vlan.get('name', '-'), justify) def output_ip(gandi, ip, datacenters, vms, ifaces, output_keys, justify=11): """ Helper to output an ip information.""" output_generic(gandi, ip, output_keys, justify) if 'type' in output_keys: iface = ifaces.get(ip['iface_id']) type_ = 'private' if iface.get('vlan') else 'public' output_line(gandi, 'type', type_, justify) if type_ == 'private': output_line(gandi, 'vlan', iface['vlan']['name'], justify) if 'vm' in output_keys: iface = ifaces.get(ip['iface_id']) vm_id = iface.get('vm_id') if vm_id: vm_name = vms.get(vm_id, {}).get('hostname') if vm_name: output_line(gandi, 'vm', vm_name, justify) if 'dc' in output_keys: for dc in datacenters: if dc['id'] == ip.get('datacenter_id', ip.get('datacenter', {}).get('id')): dc_name = dc.get('dc_code', dc.get('iso', '')) break output_line(gandi, 'datacenter', dc_name, justify) def randomstring(prefix=None): """ Helper to generate a random string, used for temporary hostnames.""" if not prefix: prefix = 'tmp' return '%s%s' % (prefix, str(int(time.time()))) def output_list(gandi, val): """Helper to generate a beautiful list.""" for element in val: gandi.echo(element) def date_handler(obj): """ Serialize date for json output """ return obj.isoformat() if hasattr(obj, 'isoformat') else str(obj) def output_json(gandi, format, value): """ Helper to show json output """ if format == 'json': gandi.echo(json.dumps(value, default=date_handler, sort_keys=True)) elif format == 'pretty-json': gandi.echo(json.dumps(value, default=date_handler, sort_keys=True, indent=2, separators=(',', ': '))) def output_sub_line(gandi, key, val, justify): """ Base helper to output a key value using left justify.""" msg = ('\t%%-%ds:%%s' % justify) % (key, (' %s' % val) if val else '') gandi.echo(msg) def output_sub_generic(gandi, data, output_keys, justify=10): """ Generic helper to output info from a data dict.""" for key in output_keys: if key in data: output_sub_line(gandi, key, data[key], justify) def output_service(gandi, service, status, justify=10): """ Helper to output a status service information.""" output_line(gandi, service, status, justify) def output_hostedcert(gandi, hcert, output_keys, justify=12): output_keys = list(output_keys) fqdns = 'fqdns' in output_keys vhosts = 'vhosts' in output_keys if fqdns: output_keys.pop(output_keys.index('fqdns')) if vhosts: output_keys.pop(output_keys.index('vhosts')) output_generic(gandi, hcert, output_keys, justify) if fqdns: for fqdn in hcert['fqdns']: gandi.separator_sub_line() output_sub_line(gandi, 'fqdn', fqdn['name'], 10) if vhosts: for vhost in hcert['related_vhosts']: gandi.separator_sub_line() output_sub_line(gandi, 'vhost', vhost['name'], 10) output_sub_line(gandi, 'type', vhost['type'], 10) def output_domain(gandi, domain, output_keys, justify=12): """ Helper to output a domain information.""" if 'nameservers' in domain: domain['nameservers'] = format_list(domain['nameservers']) if 'services' in domain: domain['services'] = format_list(domain['services']) if 'tags' in domain: domain['tags'] = format_list(domain['tags']) output_generic(gandi, domain, output_keys, justify) if 'created' in output_keys: output_line(gandi, 'created', domain['date_created'], justify) if 'expires' in output_keys: date_end = domain.get('date_registry_end') if date_end: days_left = (date_end - datetime.now()).days output_line(gandi, 'expires', '%s (in %d days)' % (date_end, days_left), justify) if 'updated' in output_keys: output_line(gandi, 'updated', domain['date_updated'], justify) def output_mailbox(gandi, mailbox, output_keys, justify=16): """ Helper to output a mailbox information.""" quota = 'quota' in output_keys responder = 'responder' in output_keys if quota: output_keys.pop(output_keys.index('quota')) if responder: output_keys.pop(output_keys.index('responder')) if 'aliases' in output_keys: mailbox['aliases'] = sorted(mailbox['aliases']) output_generic(gandi, mailbox, output_keys, justify) if 'fallback' in output_keys: output_line(gandi, 'fallback email', mailbox['fallback_email'], justify) if quota: granted = mailbox['quota']['granted'] if mailbox['quota']['granted'] == 0: granted = 'unlimited' output_line(gandi, 'quota usage', '%s KiB / %s' % (mailbox['quota']['used'], granted), justify) if responder: responder_status = 'yes' if mailbox['responder']['active'] else 'no' output_line(gandi, 'responder active', responder_status, justify) output_line(gandi, 'responder text', mailbox['responder']['text'], justify) def output_forward(gandi, domain, forward, justify=14): """ Helper to output a mail forward information.""" for dest in forward['destinations']: output_line(gandi, forward['source'], dest, justify) def output_dns_records(gandi, records, output_keys, justify=12): """ Helper to output a dns records information.""" for key in output_keys: real_key = 'rrset_%s' % key if real_key in records: val = records[real_key] if key == 'values': val = format_list(records[real_key]) output_line(gandi, key, val, justify) gandi.cli-1.2/gandi/cli/core/utils/ascii_sparks.py0000644000175000017500000000043312506776076023050 0ustar sayounsayoun00000000000000# coding: utf-8 # Author: Rory McCann # https://pypi.python.org/pypi/ascii_sparks/0.0.3 # License: Unknown parts = u' ▁▂▃▄▅▆▇▉' def sparks(nums): fraction = max(nums) / float(len(parts) - 1) return ''.join([parts[int(round(x / fraction))] for x in nums]) gandi.cli-1.2/gandi/cli/core/utils/size.py0000644000175000017500000000072112656121545021336 0ustar sayounsayoun00000000000000"""Size related methods.""" import click from gandi.cli.core.cli import compatcallback @compatcallback def disk_check_size(ctx, param, value): """ Validation callback for disk size parameter.""" if value: # if we've got a prefix if isinstance(value, tuple): val = value[1] else: val = value if val % 1024: raise click.ClickException('Size must be a multiple of 1024.') return value gandi.cli-1.2/gandi/cli/core/client.py0000644000175000017500000001007713164644614020511 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- """ XML-RPC connection helper. """ from __future__ import print_function import sys import socket try: import xmlrpclib except ImportError: import xmlrpc.client as xmlrpclib try: import requests except ImportError: print('python requests is required, please reinstall.', file=sys.stderr) sys.exit(1) from gandi.cli import __version__ from gandi.cli.core.utils.xmlrpc import RequestsTransport class APICallFailed(Exception): """ Raise when an error occurred during an API call. """ def __init__(self, errors, code=None): """ Initialize exception. """ self.errors = errors self.code = code class GandiTransport(RequestsTransport): """ Mixin to send custom User-Agent in requests.""" user_agent = 'gandi.cli/%s' % __version__ class DryRunException(APICallFailed): dry_run = None def __init__(self, message, code, dry_run): super(DryRunException, self).__init__(message, code) self.dry_run = dry_run class XMLRPCClient(object): """ Class wrapper for xmlrpc calls to Gandi public API. """ def __init__(self, host, debug=False): """ Initialize xml-rpc endpoint connector. """ self.debug = debug self.host = host self.endpoint = xmlrpclib.ServerProxy( host, allow_none=True, use_datetime=True, transport=GandiTransport(use_datetime=True, host=host)) def request(self, method, apikey, *args, **kwargs): """ Make a xml-rpc call to remote API. """ dry_run = kwargs.get('dry_run', False) return_dry_run = kwargs.get('return_dry_run', False) if return_dry_run: args[-1]['--dry-run'] = True try: func = getattr(self.endpoint, method) return func(apikey, *args) except (socket.error, requests.exceptions.ConnectionError): msg = 'Gandi API service is unreachable' raise APICallFailed(msg) except xmlrpclib.Fault as err: msg = 'Gandi API has returned an error: %s' % err if dry_run: args[-1]['--dry-run'] = True ret = func(apikey, *args) raise DryRunException(msg, err.faultCode, ret) raise APICallFailed(msg, err.faultCode) except TypeError as err: msg = 'An unknown error has occurred: %s' % err raise APICallFailed(msg) class JsonClient(object): """ Class wrapper for JSON calls. """ @classmethod def format_errors(cls, errors): return '\n'.join([err['description'] for err in errors]) @classmethod def request(cls, method, url, **kwargs): """Make a http call to a remote API and return a json response.""" user_agent = 'gandi.cli/%s' % __version__ headers = {'User-Agent': user_agent, 'Content-Type': 'application/json; charset=utf-8'} if kwargs.get('headers'): headers.update(kwargs.pop('headers')) try: response = requests.request(method, url, headers=headers, **kwargs) response.raise_for_status() try: return response.json(), response.headers except ValueError as err: return response.text, response.headers except (socket.error, requests.exceptions.ConnectionError): msg = 'Remote API service is unreachable' raise APICallFailed(msg) except Exception as err: if isinstance(err, requests.HTTPError): try: resp = response.json() except: msg = 'An unknown error has occurred: %s' % err raise APICallFailed(msg) if resp.get('message'): error = resp.get('message') if resp.get('errors'): error = cls.format_errors(resp.get('errors')) msg = '%s: %s' % (err, error) else: msg = 'An unknown error has occurred: %s' % err raise APICallFailed(msg) gandi.cli-1.2/gandi/cli/core/__init__.py0000644000175000017500000000031312441335654020760 0ustar sayounsayoun00000000000000""" Gandi CLI core modules. Contains: - XML-RPC connection handler - Configuration handler - Base class, which load modules and commands - Custom command line validation parameters - Output methods """ gandi.cli-1.2/gandi/cli/core/params.py0000644000175000017500000004345113214476173020517 0ustar sayounsayoun00000000000000""" Custom command line validation parameters. """ import re import sys import click from click.decorators import _param_memo from gandi.cli.core.base import GandiContextHelper class GandiChoice(click.Choice): """ Base class for custom Choice parameters. """ gandi = None def __init__(self): """ Initialize choices list. """ self._choices = [] def _get_choices(self, gandi): """ Internal method to get choices list """ raise NotImplementedError @property def choices(self): """ Retrieve choices from API if possible""" if not self._choices: gandi = self.gandi or GandiContextHelper() self._choices = self._get_choices(gandi) if not self._choices: api = gandi.get_api_connector() gandi.echo('Please check that you are connecting to the good ' "api '%s' and that it's running." % (api.host)) sys.exit(1) return self._choices def convert(self, value, param, ctx): """ Internal method to use correct context. """ self.gandi = ctx.obj return click.Choice.convert(self, value, param, ctx) def convert_deprecated_value(self, value): """ To override when needed """ return value class DatacenterParamType(GandiChoice): """ Choice parameter to select an available datacenter. """ name = 'datacenter' def _get_choices(self, gandi): """ Internal method to get choices list """ iso_codes = [] dc_codes = [] for item in gandi.datacenter.list(): iso_codes.append(item['iso']) if item.get('dc_code'): dc_codes.append(item['dc_code']) return dc_codes + iso_codes def convert(self, value, param, ctx): """ Convert value to uppercase. """ self.gandi = ctx.obj value = value.upper() return click.Choice.convert(self, value, param, ctx) def convert_deprecated_value(self, value): """ To update the configuration with the new datacenter naming """ convert = { 'FR': 'FR-SD2', 'LU': 'LU-BI1', 'US': 'US-BA1', } return convert.get(value, value) class PaasTypeParamType(GandiChoice): """ Choice parameter to select an available PaaS instance type. """ name = 'paas type' def _get_choices(self, gandi): """ Internal method to get choices list """ return [item['name'] for item in gandi.paas.type_list()] class IntChoice(click.Choice): """ Choice parameter to select an integer value in a set of int values.""" name = 'integer choice' def convert(self, value, param, ctx): """ Convert value to int. """ self.gandi = ctx.obj try: value = str(value) except Exception: pass value = click.Choice.convert(self, value, param, ctx) return int(value) class DiskImageParamType(GandiChoice): """ Choice parameter to select an available disk image. """ name = 'images' def _get_choices(self, gandi): """ Internal method to get choices list """ image_list = [] for item in gandi.image.list(): label = item['label'] if item['visibility'] == 'deprecated': label = '*%s' % label image_list.append(label) disk_list = [item['name'] for item in gandi.disk.list_create()] return sorted(tuple(set(image_list))) + disk_list def convert(self, value, param, ctx): """ Try to find correct disk image regarding version. """ self.gandi = ctx.obj # remove deprecated * prefix choices = [choice.replace('*', '') for choice in self.choices] value = value.replace('*', '') # Exact match if value in choices: return value # Try to find 64 bits version new_value = '%s 64 bits' % value if new_value in choices: return new_value # Try to find without specific bits version p = re.compile(' (64|32) bits') new_value = p.sub('', value) if new_value in choices: return new_value self.fail('invalid choice: %s. (choose from %s)' % (value, ', '.join(self.choices)), param, ctx) class KernelParamType(GandiChoice): """ Choice parameter to select an available kernel. """ name = 'kernels' def _get_choices(self, gandi): """ Internal method to get choices list """ kernel_families = list(gandi.kernel.list().values()) return [kernel for klist in kernel_families for kernel in klist] def convert(self, value, param, ctx): """ Try to find correct kernel regarding version. """ self.gandi = ctx.obj # Exact match first if value in self.choices: return value # Also try with x86-64 suffix new_value = '%s-x86_64' % value if new_value in self.choices: return new_value self.fail('invalid choice: %s. (choose from %s)' % (value, ', '.join(self.choices)), param, ctx) class SnapshotParamType(GandiChoice): """ Choice parameter to select an available snapshot profile. """ name = 'snapshot profile' target = None def __init__(self, target=None): """ Initialize choices list. """ self._choices = [] self.target = target def _get_choices(self, gandi): """ Internal method to get choices list """ return [str(item['id']) for item in gandi.snapshotprofile.list(target=self.target)] def convert(self, value, param, ctx): """ Convert value to int. """ self.gandi = ctx.obj value = click.Choice.convert(self, value, param, ctx) return int(value) class CertificatePackage(GandiChoice): """ Choice parameter to select an available certificate package. """ name = 'certificate package' def _get_choices(self, gandi): """ Internal method to get choices list """ return [item['name'] for item in gandi.certificate.package_list()] class CertificatePackageType(CertificatePackage): """ Choice parameter to select an available certificate package type. """ name = 'certificate package type' def _get_choices(self, gandi): """ Internal method to get choices list """ packages = super(CertificatePackageType, self)._get_choices(gandi) return list(set([pack.split('_')[1] for pack in packages])) class CertificatePackageMax(CertificatePackage): """ Choice parameter to select an available certificate package max altname. """ name = 'certificate package max' def _get_choices(self, gandi): """ Internal method to get choices list """ packages = super(CertificatePackageMax, self)._get_choices(gandi) ret = list(set([pack.split('_')[2] for pack in packages])) if 'w' in ret: ret.remove('w') return ret def convert(self, value, param, ctx): """ Convert value to int. """ self.gandi = ctx.obj value = click.Choice.convert(self, value, param, ctx) return int(value) class CertificatePackageWarranty(CertificatePackage): """ Choice parameter to select an available certificate warranty. """ name = 'certificate package warranty' def _get_choices(self, gandi): """ Internal method to get choices list """ packages = super(CertificatePackageWarranty, self)._get_choices(gandi) return list(set([pack.split('_')[3] for pack in packages])) def convert(self, value, param, ctx): """ Convert value to int. """ self.gandi = ctx.obj value = click.Choice.convert(self, value, param, ctx) return int(value) class CertificateDcvMethod(click.Choice): """ Choice parameter to select a certificate dcv method. * 'email' will send you an email to check domain ownership * 'dns' will require you to add a TXT record in your domain zone * 'file' will require you to add a file on you server * 'auto' can only be used when your domain and its zone are on the same gandi account you are currently using (gandi will add the TXT dns record). """ name = 'certificate dcv method' choices = ['email', 'dns', 'file', 'auto'] def __init__(self): """ Initialize choices list. """ pass class IpType(click.Choice): """ Choice parameter to filter on ip types. * 'private' will only retrieve private ips * 'public' will only retrieve public ips """ name = 'ip type' choices = ['private', 'public'] def __init__(self): """ Initialize choices list. """ pass class StringConstraint(click.types.StringParamType): """ Check that provided string matches constraints.""" name = 'string constraints' def __init__(self, minlen=None, maxlen=None): self.min = minlen self.max = maxlen def convert(self, value, param, ctx): value = click.types.StringParamType.convert(self, value, param, ctx) rv = len(value) if self.min is not None and rv < self.min or \ self.max is not None and rv > self.max: if self.min is None: self.fail('%s is longer than the maximum valid length ' '%s.' % (rv, self.max), param, ctx) elif self.max is None: self.fail('%s is shorter than the minimum valid length ' '%s.' % (rv, self.min), param, ctx) else: self.fail('%s is not in the valid length range of %s to %s.' % (rv, self.min, self.max), param, ctx) return value def __repr__(self): return 'StringConstraint(%r, %r)' % (self.min, self.max) class EmailParamType(click.ParamType): """Check the email value and return a list ['login', 'domain']. """ name = 'email' def convert(self, value, param, ctx): """ Validate value using regexp. """ rxp = '^[^@]+?@[-.a-z0-9]+$' regex = re.compile(rxp, re.I) try: if regex.match(value): value = value.split("@") return value else: self.fail('%s is not a valid email address' % value, param, ctx) except ValueError: self.fail('%s is not a valid email address' % value, param, ctx) class SizeParamType(click.ParamType): name = 'size' suffixes = {'M': 0, 'G': 1, 'T': 2} prefixes = ['+'] def convert(self, value, param, ctx): prefix = '' suffix = '' for i, c in enumerate(value): if not c.isdigit(): if c in self.prefixes: prefix = c continue suffix = value[i:] value = value[:i] break try: mul = self.suffixes[suffix] if suffix else 0 return prefix, int(value) * (1 << (mul * 10)) except ValueError: self.fail("%r is not an integer" % (value)) except KeyError: self.fail("%r is not a supported suffix" % (suffix)) class BackendParamType(click.ParamType): """ Check the validity of the server ip and port and return a dict ['ip', 'port']. """ name = 'backend' def convert(self, value, param, ctx): """ Validate value using regexp. """ rxp = "^(((([1]?\d)?\d|2[0-4]\d|25[0-5])\.){3}(([1]?\d)?\d|2[0-4]\d|"\ "25[0-5]))|([\da-fA-F]{1,4}(\:[\da-fA-F]{1,4}){7})|(([\da-fA-F]"\ "{1,4}:){0,5}::([\da-fA-F]{1,4}:)"\ "{0,5}[\da-fA-F]{1,4})$" regex = re.compile(rxp, re.I) backend = {} if value.count(':') == 0: # port is not set backend['ip'] = value elif value.count(':') == 7: # it's an ipv6 without port backend['ip'] = value elif value.count(':') == 8: # it's an ipv6 with port backend['ip'] = value.rsplit(':', 1)[0] backend['port'] = int(value.rsplit(':', 1)[1]) else: backend['ip'] = value.split(':')[0] backend['port'] = int(value.split(':')[1]) try: if regex.match(backend['ip']): return backend else: self.fail('%s is not a valid ip address' % backend['ip'], param, ctx) except ValueError: self.fail('%s is not a valid ip address' % backend['ip'], param, ctx) class WebAccNameParamType(GandiChoice): """ Choice a webaccelerator """ name = 'webacc list' def _get_choices(self, gandi): """ Internal method to get choice list """ return [str(item['name']) for item in gandi.webacc.list()] class WebAccVhostParamType(GandiChoice): """ Retrieve vhost on a webaccelerator """ name = 'webacc vhost list' def _get_choices(self, gandi): """ Internal method to get choice list """ return [str(item['name']) for item in gandi.webacc.vhost_list()] class OperStepParamType(click.Choice): """ Choice parameter to filter on operation step """ name = 'oper step' choices = ['BILL', 'WAIT', 'RUN', 'ERROR'] def __init__(self): """ Initialize choices list. """ pass class DNSRecordsParamType(GandiChoice): """ Choice parameter to select a DNS record type. """ name = 'dns record' def _get_choices(self, gandi): """ Internal method to get choices list """ return [item for item in gandi.dns.type_list()] def convert(self, value, param, ctx): """ Convert value to uppercase. """ self.gandi = ctx.obj value = value.upper() return click.Choice.convert(self, value, param, ctx) DATACENTER = DatacenterParamType() PAAS_TYPE = PaasTypeParamType() DISK_IMAGE = DiskImageParamType() DISK_MAXLIST = 500 KERNEL = KernelParamType() SNAPSHOTPROFILE_PAAS = SnapshotParamType('paas') SNAPSHOTPROFILE_VM = SnapshotParamType('vm') CERTIFICATE_PACKAGE = CertificatePackage() CERTIFICATE_PACKAGE_TYPE = CertificatePackageType() CERTIFICATE_PACKAGE_MAX = CertificatePackageMax() CERTIFICATE_PACKAGE_WARRANTY = CertificatePackageWarranty() CERTIFICATE_DCV_METHOD = CertificateDcvMethod() EMAIL_TYPE = EmailParamType() IP_TYPE = IpType() SIZE = SizeParamType() BACKEND = BackendParamType() WEBACC_NAME = WebAccNameParamType() WEBACC_VHOST_NAME = WebAccVhostParamType() OPER_STEP = OperStepParamType() DNS_RECORDS = DNSRecordsParamType() class GandiOption(click.Option): """ Custom command option class for handling configuration files. When no value was found on command line, try to pull it from configuration Display default or configuration value when needed """ def display_value(self, ctx, value): """ Display value to be used for this parameter. """ gandi = ctx.obj gandi.log('%s: %s' % (self.name, (value if value is not None else 'Not found'))) def get_default(self, ctx): """ Retrieve default value and display it when prompt disabled. """ value = click.Option.get_default(self, ctx) if not self.prompt: # value found in default display it self.display_value(ctx, value) return value def consume_value(self, ctx, opts): """ Retrieve default value and display it when prompt is disabled. """ value = click.Option.consume_value(self, ctx, opts) if not value: # value not found by click on command line # now check using our context helper in order into # local configuration # global configuration gandi = ctx.obj value = gandi.get(self.name) if value is not None: # value found in configuration display it self.display_value(ctx, value) else: if self.default is None and self.required: metavar = '' if self.type.name not in ['integer', 'text']: metavar = self.make_metavar() prompt = '%s %s' % (self.help, metavar) gandi.echo(prompt) return value def handle_parse_result(self, ctx, opts, args): """ Save value for this option in configuration if key/value pair doesn't already exist. Or old value in config was deprecated it needs to be updated to the new value format but the value keeps the same "meaning" """ gandi = ctx.obj needs_update = False value, args = click.Option.handle_parse_result(self, ctx, opts, args) if value is not None: previous_value = gandi.get(global_=True, key=self.name) if isinstance(self.type, GandiChoice): if value == previous_value: needs_update = True value = self.type.convert_deprecated_value(value) if not previous_value or needs_update: gandi.configure(global_=True, key=self.name, val=value) opts[self.name] = value value, args = click.Option.handle_parse_result(self, ctx, opts, args) return value, args def option(*param_decls, **attrs): """Attach an option to the command. All positional arguments are passed as parameter declarations to :class:`Option`, all keyword arguments are forwarded unchanged. This is equivalent to creating an :class:`Option` instance manually and attaching it to the :attr:`Command.params` list. """ def decorator(f): _param_memo(f, GandiOption(param_decls, **attrs)) return f return decorator # create a decorator to pass the Gandi object as context to click calls pass_gandi = click.make_pass_decorator(GandiContextHelper, ensure=True) gandi.cli-1.2/gandi/cli/core/conf.py0000644000175000017500000002133113160664756020160 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- """ Configuration handler class declaration. """ import os import sys import yaml import os.path from distutils.dir_util import mkpath try: from yaml import CSafeLoader as YAMLLoader except ImportError: from yaml import SafeLoader as YAMLLoader import click class GandiConfig(object): """ Base class for yaml configuration. Manage - read/write configuration files/values - handle two scopes : global/local """ _conffiles = {} home_config = os.environ.get('GANDI_CONFIG', '~/.config/gandi/config.yaml') local_config = '.gandi.config.yaml' apienvs = { 'ote': 'https://rpc.ote.gandi.net/xmlrpc/', 'production': 'https://rpc.gandi.net/xmlrpc/', } default_apienv = 'production' @classmethod def load_config(cls): """ Load global and local configuration files and update if needed.""" config_file = os.path.expanduser(cls.home_config) global_conf = cls.load(config_file, 'global') cls.load(cls.local_config, 'local') # update global configuration if needed cls.update_config(config_file, global_conf) @classmethod def update_config(cls, config_file, config): """ Update configuration if needed. """ need_save = False # delete old env key if 'api' in config and 'env' in config['api']: del config['api']['env'] need_save = True # convert old ssh_key configuration entry ssh_key = config.get('ssh_key') sshkeys = config.get('sshkey') if ssh_key and not sshkeys: config.update({'sshkey': [ssh_key]}) need_save = True elif ssh_key and sshkeys: config.update({'sshkey': sshkeys.append(ssh_key)}) need_save = True # remove old value if ssh_key: del config['ssh_key'] need_save = True # save to disk if need_save: cls.save(config_file, config) @classmethod def load(cls, filename, name=None): """ Load yaml configuration from filename. """ if not os.path.exists(filename): return {} name = name or filename if name not in cls._conffiles: with open(filename) as fdesc: content = yaml.load(fdesc, YAMLLoader) # in case the file is empty if content is None: content = {} cls._conffiles[name] = content return cls._conffiles[name] @classmethod def save(cls, filename, config): """ Save configuration to yaml file. """ mode = os.O_WRONLY | os.O_TRUNC | os.O_CREAT with os.fdopen(os.open(filename, mode, 0o600), 'w') as fname: yaml.safe_dump(config, fname, indent=4, default_flow_style=False) @classmethod def _del(cls, scope, key, separator='.', conf=None): orig_key = key key = key.split(separator) if not conf: conf = cls._conffiles.get(scope, {}) if separator not in orig_key: if orig_key in conf: del conf[orig_key] return for k in key: if k not in conf: return else: cls._del(scope, separator.join([k1 for k1 in key if k1 != k]), conf=conf[k]) return @classmethod def delete(cls, global_, key): """ Delete key/value pair from configuration file. """ # first retrieve current configuration scope = 'global' if global_ else 'local' config = cls._conffiles.get(scope, {}) cls._del(scope, key) conf_file = cls.home_config if global_ else cls.local_config # save configuration to file cls.save(os.path.expanduser(conf_file), config) @classmethod def _set(cls, scope, key, val, separator='.'): orig_key = key key = key.split(separator) value = cls._conffiles.get(scope, {}) if separator not in orig_key: value[orig_key] = val return for k in key: if k not in value: value[k] = {} last_val = value value = value[k] else: last_val = value value = value[k] last_val[k] = val @classmethod def _get(cls, scope, key, default=None, separator='.'): key = key.split(separator) value = cls._conffiles.get(scope, {}) if not value: return default try: for k in key: value = value[k] return value except KeyError: return default @classmethod def get(cls, key, default=None, separator='.', global_=False): """ Retrieve a key value from loaded configuration. Order of search if global_=False: 1/ environnment variables 2/ local configuration 3/ global configuration """ # first check environnment variables # if we're not in global scope if not global_: ret = os.environ.get(key.upper().replace('.', '_')) if ret is not None: return ret # then check in local and global configuration unless global_=True scopes = ['global'] if global_ else ['local', 'global'] for scope in scopes: ret = cls._get(scope, key, default, separator) if ret is not None and ret != default: return ret if ret is None or ret == default: return default @classmethod def configure(cls, global_, key, val): """ Update and save configuration value to file. """ # first retrieve current configuration scope = 'global' if global_ else 'local' if scope not in cls._conffiles: cls._conffiles[scope] = {} config = cls._conffiles.get(scope, {}) # apply modification to fields cls._set(scope, key, val) conf_file = cls.home_config if global_ else cls.local_config # save configuration to file cls.save(os.path.expanduser(conf_file), config) @classmethod def list(cls, global_): """ Return configuration file content. """ scope = 'global' if global_ else 'local' return cls._conffiles.get(scope, {}) @classmethod def init_config(cls): """ Initialize Gandi CLI configuration. Create global configuration directory with API credentials """ try: # first load current conf and only overwrite needed params # we don't want to reset everything config_file = os.path.expanduser(cls.home_config) config = cls.load(config_file, 'global') cls._del('global', 'api.env') hidden_apikey = '%s...' % cls.get('api.key', '')[:6] apikey = click.prompt('Api key (xmlrpc)', default=hidden_apikey) if apikey == hidden_apikey: # if default value then use actual value not hidden one apikey = cls.get('api.key') env_choice = click.Choice(list(cls.apienvs.keys())) apienv = click.prompt('Environnment [production]/ote', default=cls.default_apienv, type=env_choice, show_default=False) sshkey = click.prompt('SSH keyfile', default='~/.ssh/id_rsa.pub') hidden_apikeyrest = '%s...' % cls.get('apirest.key', '')[:6] apikeyrest = click.prompt('Api key (REST)', default=hidden_apikeyrest) if apikeyrest == hidden_apikeyrest: # if default value then use actual value not hidden one apikeyrest = cls.get('apirest.key') config.update({ 'api': {'key': apikey, 'host': cls.apienvs[apienv]}, }) if apikeyrest: config.update({ 'apirest': {'key': apikeyrest}, }) if sshkey is not None: sshkey_file = os.path.expanduser(sshkey) if os.path.exists(sshkey_file): config['sshkey'] = [sshkey_file] directory = os.path.expanduser(os.path.dirname(config_file)) if not os.path.exists(directory): mkpath(directory, 0o700) # save to disk cls.save(config_file, config) # load in memory cls.load(config_file, 'global') except (KeyboardInterrupt, click.exceptions.Abort): cls.echo('Aborted.') sys.exit(1) gandi.cli-1.2/gandi/cli/__main__.py0000644000175000017500000000022412441335654020012 0ustar sayounsayoun00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from gandi.cli.core.cli import cli def main(): cli(obj={}) if __name__ == "__main__": main() gandi.cli-1.2/gandi/cli/__init__.py0000644000175000017500000000005513227414723020031 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- __version__ = '1.2' gandi.cli-1.2/gandi/cli/commands/0000755000175000017500000000000013227415174017522 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi/cli/commands/snapshotprofile.py0000644000175000017500000000265613164644514023327 0ustar sayounsayoun00000000000000""" Snapshot profiles namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_snapshot_profile from gandi.cli.core.params import pass_gandi @cli.group(name='snapshotprofile') @pass_gandi def snapshotprofile(gandi): """Commands related to hosting snapshot profiles.""" @snapshotprofile.command() @click.option('--only-paas', help='Only display PaaS profiles.', is_flag=True) @click.option('--only-vm', help='Only display vm profile.s', is_flag=True) @pass_gandi def list(gandi, only_paas, only_vm): """ List snapshot profiles. """ target = None if only_paas and not only_vm: target = 'paas' if only_vm and not only_paas: target = 'vm' output_keys = ['id', 'name', 'kept_total', 'target'] result = gandi.snapshotprofile.list({}, target=target) for num, profile in enumerate(result): if num: gandi.separator_line() output_snapshot_profile(gandi, profile, output_keys) return result @snapshotprofile.command() @click.argument('resource') @pass_gandi def info(gandi, resource): """ Display information about a snapshot profile. Resource can be a profile name or ID """ output_keys = ['id', 'name', 'kept_total', 'target', 'quota_factor', 'schedules'] result = gandi.snapshotprofile.info(resource) output_snapshot_profile(gandi, result, output_keys) return result gandi.cli-1.2/gandi/cli/commands/disk.py0000644000175000017500000003461213227142745021035 0ustar sayounsayoun00000000000000""" Disk namespace commands. """ import click from click.exceptions import UsageError from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_disk, output_generic, randomstring, DatacenterLimited ) from gandi.cli.core.utils.size import disk_check_size from gandi.cli.core.params import (pass_gandi, DATACENTER, SNAPSHOTPROFILE_VM, KERNEL, SIZE, option, DISK_IMAGE) @cli.group(name='disk') @pass_gandi def disk(gandi): """Commands related to hosting disks.""" @disk.command() @click.option('--only-data', help='Only display data disks.', is_flag=True) @click.option('--only-snapshot', help='Only display snapshots.', is_flag=True) @click.option('--attached', help='Only display disks attached to a VM', is_flag=True) @click.option('--detached', help='Only display detached disks', is_flag=True) @click.option('--type', help='Display types.', is_flag=True) @click.option('--id', help='Display ids.', is_flag=True) @click.option('--vm', help='Display vms.', is_flag=True) @click.option('--snapshotprofile', help='Display snapshot profile.', is_flag=True) @click.option('--datacenter', default=None, type=DATACENTER, help='Filter results by datacenter.') @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, only_data, only_snapshot, attached, detached, type, id, vm, snapshotprofile, datacenter, limit): """ List disks. """ options = { 'items_per_page': limit, } if attached and detached: raise UsageError('You cannot use both --attached and --detached.') if only_data: options.setdefault('type', []).append('data') if only_snapshot: options.setdefault('type', []).append('snapshot') if datacenter: options['datacenter_id'] = gandi.datacenter.usable_id(datacenter) output_keys = ['name', 'state', 'size'] if type: output_keys.append('type') if id: output_keys.append('id') if vm: output_keys.append('vm') profiles = [] if snapshotprofile: output_keys.append('profile') profiles = gandi.snapshotprofile.list() result = gandi.disk.list(options) vms = dict([(vm_['id'], vm_) for vm_ in gandi.iaas.list()]) # filter results per attached/detached disks = [] for disk in result: if attached and not disk['vms_id']: continue if detached and disk['vms_id']: continue disks.append(disk) for num, disk in enumerate(disks): if num: gandi.separator_line() output_disk(gandi, disk, [], vms, profiles, output_keys) return result @disk.command() @click.argument('resource', nargs=-1, required=True) @pass_gandi def info(gandi, resource): """ Display information about a disk. Resource can be a disk name or ID """ output_keys = ['name', 'state', 'size', 'type', 'id', 'dc', 'vm', 'profile', 'kernel', 'cmdline'] resource = sorted(tuple(set(resource))) vms = dict([(vm['id'], vm) for vm in gandi.iaas.list()]) datacenters = gandi.datacenter.list() result = [] for num, item in enumerate(resource): if num: gandi.separator_line() disk = gandi.disk.info(item) output_disk(gandi, disk, datacenters, vms, [], output_keys) result.append(disk) return result @disk.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi @click.argument('resource', nargs=-1, required=True) def detach(gandi, resource, background, force): """ Detach disks from currectly attached vm. Resource can be a disk name, or ID """ resource = sorted(tuple(set(resource))) if not force: proceed = click.confirm('Are you sure you want to detach %s?' % ', '.join(resource)) if not proceed: return result = gandi.disk.detach(resource, background) if background: gandi.pretty_echo(result) return result @disk.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('-r', '--read-only', default=False, is_flag=True, help='Attach disk as read-only') @click.option('--position', '-p', type=click.INT, help='Position where disk should be attached: 0 for system disk.' ' If there is already a disk attached at the specified' ' position, it will be swapped.') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi @click.argument('disk', nargs=1, required=True) @click.argument('vm', nargs=1, required=True) def attach(gandi, disk, vm, position, read_only, background, force): """ Attach disk to vm. disk can be a disk name, or ID vm can be a vm name, or ID """ if not force: proceed = click.confirm("Are you sure you want to attach disk '%s'" " to vm '%s'?" % (disk, vm)) if not proceed: return disk_info = gandi.disk.info(disk) attached = disk_info.get('vms_id', False) if attached and not force: gandi.echo('This disk is still attached') proceed = click.confirm('Are you sure you want to detach %s?' % disk) if not proceed: return result = gandi.disk.attach(disk, vm, background, position, read_only) if background and result: gandi.pretty_echo(result) return result @disk.command() @click.option('--cmdline', type=click.STRING, default=None, help='Kernel cmdline.') @click.option('--kernel', type=KERNEL, default=None, help='Kernel for disk.') @click.option('--name', type=click.STRING, default=None, help='Disk name.') @click.option('--size', default=None, metavar='[+]SIZE[M|G|T]', type=SIZE, help=('Disk size. A size suffix (M for megabytes up to T for ' 'terabytes) is optional, megabytes is the default if no ' 'suffix is present. A prefix + is optionnal, if provided ' 'size value will be added to current disk size, default ' 'is to set directly new disk size.'), callback=disk_check_size) @click.option('--snapshotprofile', help='Selected snapshot profile.', default=None, type=SNAPSHOTPROFILE_VM) @click.option('--delete-snapshotprofile', default=False, is_flag=True, help='Remove snapshot profile associated to this disk.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @pass_gandi @click.argument('resource') def update(gandi, resource, cmdline, kernel, name, size, snapshotprofile, delete_snapshotprofile, background): """ Update a disk. Resource can be a disk name, or ID """ if snapshotprofile and delete_snapshotprofile: raise UsageError('You must not set snapshotprofile and ' 'delete-snapshotprofile.') if delete_snapshotprofile: snapshotprofile = '' if kernel: source_info = gandi.disk.info(resource) available = gandi.kernel.is_available(source_info, kernel) if not available: raise UsageError('Kernel %s is not available for disk %s' % (kernel, resource)) result = gandi.disk.update(resource, name, size, snapshotprofile, background, cmdline, kernel) if background: gandi.pretty_echo(result) return result @disk.command() @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.option('--bg', '--background', default=False, is_flag=True, help='run command in background mode (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def delete(gandi, resource, force, background): """ Delete a disk. """ output_keys = ['id', 'type', 'step'] resource = sorted(tuple(set(resource))) if not force: disk_info = "'%s'" % ', '.join(resource) proceed = click.confirm('Are you sure you want to delete disk %s?' % disk_info) if not proceed: return opers = gandi.disk.delete(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @disk.command() @click.option('--name', type=click.STRING, default=None, help='Disk name, will be generated if not provided.') @click.option('--vm', default=None, type=click.STRING, help='Attach the newly created disk to the vm.') @click.option('--size', default='3072', metavar='SIZE[M|G|T]', type=SIZE, help=('Disk size. A size suffix (M for megabytes up to T for ' 'terabytes) is optional, megabytes is the default if no ' 'suffix is present.'), callback=disk_check_size) @click.option('--snapshotprofile', help='Selected snapshot profile.', default=None, type=SNAPSHOTPROFILE_VM) @click.option('--source', default=None, type=DISK_IMAGE, help='Create a disk from a disk or a snapshot.') @option('--datacenter', type=DATACENTER, default='FR-SD5', help='Datacenter where the disk will be created.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @pass_gandi def create(gandi, name, vm, size, snapshotprofile, datacenter, source, background): """ Create a new disk. """ try: gandi.datacenter.is_opened(datacenter, 'iaas') except DatacenterLimited as exc: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) if vm: vm_dc = gandi.iaas.info(vm) vm_dc_id = vm_dc['datacenter_id'] dc_id = int(gandi.datacenter.usable_id(datacenter)) if vm_dc_id != dc_id: gandi.echo('/!\ VM %s datacenter will be used instead of %s.' % (vm, datacenter)) datacenter = vm_dc_id output_keys = ['id', 'type', 'step'] name = name or randomstring('vdi') disk_type = 'data' oper = gandi.disk.create(name, vm, size, snapshotprofile, datacenter, source, disk_type, background) if background: output_generic(gandi, oper, output_keys) return oper @disk.command() @click.option('--name', type=click.STRING, default=None, help='Snapshot name, will be generated if not provided.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource') @pass_gandi def snapshot(gandi, name, resource, background): """ Create a snapshot on the fly. """ name = name or randomstring('snp') source_info = gandi.disk.info(resource) datacenter = source_info['datacenter_id'] result = gandi.disk.create(name, None, None, None, datacenter, resource, 'snapshot', background) if background: gandi.pretty_echo(result) return result @disk.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', required=True) @pass_gandi def rollback(gandi, resource, background): """ Rollback a disk from a snapshot. """ result = gandi.disk.rollback(resource, background) if background: gandi.pretty_echo(result) return result @disk.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('resource', required=True) @pass_gandi def migrate(gandi, resource, force, background): """ Migrate a disk to another datacenter. """ # check it's not attached source_info = gandi.disk.info(resource) if source_info['vms_id']: click.echo('Cannot start the migration: disk %s is attached. ' 'Please detach the disk before starting the migration.' % resource) return disk_datacenter = source_info['datacenter_id'] dc_choices = gandi.datacenter.list_migration_choice(disk_datacenter) if not dc_choices: click.echo('No datacenter is available for migration') return elif len(dc_choices) == 1: # use the only one available datacenter_id = dc_choices[0]['id'] else: choice_list = [dc['dc_code'] for dc in dc_choices] dc_choice = click.Choice(choice_list) dc_chosen = click.prompt('Select a datacenter [%s]' % '|'.join(choice_list), # noqa type=dc_choice, show_default=True) datacenter_id = [dc['id'] for dc in dc_choices if dc['dc_code'] == dc_chosen][0] if not force: proceed = click.confirm('Are you sure you want to migrate disk %s ?' % resource) if not proceed: return datacenters = gandi.datacenter.list() dc_from = [dc['dc_code'] for dc in datacenters if dc['id'] == disk_datacenter][0] dc_to = [dc['dc_code'] for dc in datacenters if dc['id'] == datacenter_id][0] migration_msg = ('* Starting the migration of disk %s from datacenter %s ' 'to %s' % (resource, dc_from, dc_to)) gandi.echo(migration_msg) output_keys = ['id', 'type', 'step'] oper = gandi.disk.migrate(resource, datacenter_id, background) if background: output_generic(gandi, oper, output_keys) return oper gandi.cli-1.2/gandi/cli/commands/docker.py0000644000175000017500000000237413164644514021353 0ustar sayounsayoun00000000000000""" Docker namespace commands. """ import os import click from gandi.cli.core.cli import cli from gandi.cli.core.params import pass_gandi @cli.command() @click.option('--vm', help='Use given VM for docker connection') @click.argument('args', nargs=-1) @pass_gandi def docker(gandi, vm, args): """ Manage docker instance """ if not [basedir for basedir in os.getenv('PATH', '.:/usr/bin').split(':') if os.path.exists('%s/docker' % basedir)]: gandi.echo("""'docker' not found in $PATH, required for this command \ to work See https://docs.docker.com/installation/#installation to install, or use: # curl https://get.docker.io/ | sh""") return if vm: gandi.configure(True, 'dockervm', vm) else: vm = gandi.get('dockervm') if not vm: gandi.echo(""" No docker vm specified. You can create one: $ gandi vm create --hostname docker --image "Ubuntu 14.04 64 bits LTS (HVM)" \\ --run 'wget -O - https://get.docker.io/ | sh' Then configure it using: $ gandi docker --vm docker ps Or to both change target vm and spawn a process (note the -- separator): $ gandi docker --vm myvm -- run -i -t debian bash """) # noqa return return gandi.docker.handle(vm, args) gandi.cli-1.2/gandi/cli/commands/vm.py0000644000175000017500000004503113227414355020521 0ustar sayounsayoun00000000000000""" Virtual machines namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_vm, output_image, output_generic, output_datacenter, output_kernels, output_metric, DatacenterLimited ) from gandi.cli.core.utils.size import disk_check_size from gandi.cli.core.utils.password import mkpassword from gandi.cli.core.params import ( pass_gandi, option, IntChoice, DATACENTER, DISK_IMAGE, SIZE ) @cli.group(name='vm') @pass_gandi def vm(gandi): """Commands related to hosting virtual machines.""" @vm.command() @click.option('--state', default=None, help='Filter results by state.') @click.option('--datacenter', default=None, type=DATACENTER, help='Filter results by datacenter.') @click.option('--id', help='Display ids.', is_flag=True) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, state, id, limit, datacenter): """List virtual machines.""" options = { 'items_per_page': limit, } if state: options['state'] = state if datacenter: options['datacenter_id'] = gandi.datacenter.usable_id(datacenter) output_keys = ['hostname', 'state'] if id: output_keys.append('id') result = gandi.iaas.list(options) for num, vm in enumerate(result): if num: gandi.separator_line() output_vm(gandi, vm, [], output_keys) return result @vm.command() @click.argument('resource', nargs=-1, required=True) @click.option('--stat', default=False, is_flag=True, help='Display general vm statistic') @pass_gandi def info(gandi, resource, stat): """Display information about a virtual machine. Resource can be a Hostname or an ID """ output_keys = ['hostname', 'state', 'cores', 'memory', 'console', 'datacenter', 'ip'] justify = 14 if stat is True: sampler = {'unit': 'hours', 'value': 1, 'function': 'max'} time_range = 3600 * 24 query_vif = 'vif.bytes.all' query_vbd = 'vbd.bytes.all' resource = sorted(tuple(set(resource))) datacenters = gandi.datacenter.list() ret = [] for num, item in enumerate(resource): if num: gandi.separator_line() vm = gandi.iaas.info(item) output_vm(gandi, vm, datacenters, output_keys, justify) ret.append(vm) for num, disk in enumerate(vm['disks']): gandi.echo('') disk_out_keys = ['label', 'kernel_version', 'name', 'size'] output_image(gandi, disk, datacenters, disk_out_keys, justify, warn_deprecated=False) if stat is True: metrics_vif = gandi.metric.query(vm['id'], time_range, query_vif, 'vm', sampler) metrics_vbd = gandi.metric.query(vm['id'], time_range, query_vbd, 'vm', sampler) gandi.echo('') gandi.echo('vm network stats') output_metric(gandi, metrics_vif, 'direction', justify) gandi.echo('disk network stats') output_metric(gandi, metrics_vbd, 'direction', justify) return ret @vm.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def stop(gandi, background, resource): """Stop a virtual machine. Resource can be a Hostname or an ID """ output_keys = ['id', 'type', 'step'] resource = sorted(tuple(set(resource))) opers = gandi.iaas.stop(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @vm.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def start(gandi, background, resource): """Start a virtual machine. Resource can be a Hostname or an ID """ output_keys = ['id', 'type', 'step'] resource = sorted(tuple(set(resource))) opers = gandi.iaas.start(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @vm.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def reboot(gandi, background, resource): """Reboot a virtual machine. Resource can be a Hostname or an ID """ output_keys = ['id', 'type', 'step'] resource = sorted(tuple(set(resource))) opers = gandi.iaas.reboot(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @vm.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def delete(gandi, background, force, resource): """Delete a virtual machine. Resource can be a Hostname or an ID """ output_keys = ['id', 'type', 'step'] resource = sorted(tuple(set(resource))) possible_resources = gandi.iaas.resource_list() for item in resource: if item not in possible_resources: gandi.echo('Sorry virtual machine %s does not exist' % item) gandi.echo('Please use one of the following: %s' % possible_resources) return if not force: instance_info = "'%s'" % ', '.join(resource) proceed = click.confirm("Are you sure to delete Virtual Machine %s?" % instance_info) if not proceed: return iaas_list = gandi.iaas.list() stop_opers = [] for item in resource: vm = next((vm for (index, vm) in enumerate(iaas_list) if vm['hostname'] == item), gandi.iaas.info(item)) if vm['state'] == 'running': if background: gandi.echo('Virtual machine not stopped, background option ' 'disabled') background = False oper = gandi.iaas.stop(item, background) if not background: stop_opers.append(oper) opers = gandi.iaas.delete(resource, background) if background: for oper in stop_opers + opers: output_generic(gandi, oper, output_keys) return opers @vm.command() @option('--datacenter', type=DATACENTER, default='FR-SD5', help='Datacenter where the VM will be spawned.') @option('--memory', type=click.INT, default=256, help='Quantity of RAM in Megabytes to allocate.') @option('--cores', type=click.INT, default=1, help='Number of cpu.') @click.option('--ip-version', type=IntChoice(['4', '6']), default=None, help='Version of created IP.') @option('--bandwidth', type=click.INT, default=102400, help="Network bandwidth in kbit/s used to create the VM's first " "network interface.") @click.option('--login', default=None, help='Login to create on the VM.') @click.option('--password', default=False, is_flag=True, help='Will ask for a password to be set for the root account ' 'and the created login.') @click.option('--hostname', default=None, help='Hostname of the VM, will be generated if not provided.') @option('--image', type=DISK_IMAGE, default='Debian 8', help='Disk image used to boot the VM. A * prefix means the image is ' 'deprecated and will soon be unavailable.') @click.option('--run', default=None, help='Shell command that will run at the first startup of a VM.' 'This command will run with root privileges in the ``/`` ' 'directory at the end of its boot: network interfaces and ' 'disks are mounted.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @option('--sshkey', multiple=True, help='Authorize ssh authentication for the given ssh key.') @click.option('--size', default=None, metavar='SIZE[M|G|T]', type=SIZE, help=('Disk size. A size suffix (M for megabytes up to T for ' 'terabytes) is optional, megabytes is the default if no ' 'suffix is present.'), callback=disk_check_size) @click.option('--vlan', default=None, help='A vlan to use with this vm.') @click.option('--ip', default=None, help='An ip in the vlan for this vm.') @click.option('--script', default=None, help='Local script to upload and run on the VM after creation.') @click.option('--script-args', default=None, help='Local script argument line.') @click.option('--ssh', default=False, is_flag=True, help='Open a SSH session to the machine after creation ' '(default=False).') @click.option('--gen-password', default=False, is_flag=True, help='Generate a random password to be set for the root ' 'account and the created login(default=False).') @pass_gandi def create(gandi, datacenter, memory, cores, ip_version, bandwidth, login, password, hostname, image, run, background, sshkey, size, vlan, ip, script, script_args, ssh, gen_password): """Create a new virtual machine. you can specify a configuration entry named 'sshkey' containing path to your sshkey file $ gandi config set [-g] sshkey ~/.ssh/id_rsa.pub or getting the sshkey "my_key" from your gandi ssh keyring $ gandi config set [-g] sshkey my_key to know which disk image label (or id) to use as image $ gandi vm images """ try: gandi.datacenter.is_opened(datacenter, 'iaas') except DatacenterLimited as exc: if exc.date: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) if gandi.image.is_deprecated(image, datacenter): gandi.echo('/!\ Image %s is deprecated and will soon be unavailable.' % image) pwd = None if gen_password: pwd = mkpassword() if password or (not pwd and not sshkey): pwd = click.prompt('password', hide_input=True, confirmation_prompt=True) if ip and not vlan: gandi.echo("--ip can't be used without --vlan.") return if not vlan and not ip_version: ip_version = 6 if not ip_version: gandi.echo("* Private only ip vm (can't enable emergency web console " 'access).') # Display a short summary for creation if login: user_summary = 'root and %s users' % login password_summary = 'Users root and %s' % login else: user_summary = 'root user' password_summary = 'User root' gandi.echo('* %s will be created.' % user_summary) if sshkey: gandi.echo('* SSH key authorization will be used.') if pwd and gen_password: gandi.echo('* %s setup with password %s' % (password_summary, pwd)) if not pwd: gandi.echo('* No password supplied for vm (required to enable ' 'emergency web console access).') result = gandi.iaas.create(datacenter, memory, cores, ip_version, bandwidth, login, pwd, hostname, image, run, background, sshkey, size, vlan, ip, script, script_args, ssh) if background: gandi.echo('* IAAS backend is now creating your VM and its ' 'associated resources in the background.') return result @vm.command() @click.option('--memory', type=click.INT, default=None, help='Quantity of RAM in Megabytes to allocate.') @click.option('--cores', type=click.INT, default=None, help='Number of cpu.') @click.option('--console', default=None, is_flag=True, help='Activate the emergency console.') @click.option('--password', default=False, is_flag=True, help='Will ask for a password to be set for the root account ' 'and the created login.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--reboot', default=False, is_flag=True, help='Accept a VM reboot for non-live updates') @click.argument('resource') @pass_gandi def update(gandi, resource, memory, cores, console, password, background, reboot): """Update a virtual machine. Resource can be a Hostname or an ID """ pwd = None if password: pwd = click.prompt('password', hide_input=True, confirmation_prompt=True) max_memory = None if memory: max_memory = gandi.iaas.required_max_memory(resource, memory) if max_memory and not reboot: gandi.echo('memory update must be done offline.') if not click.confirm("reboot machine %s?" % resource): return result = gandi.iaas.update(resource, memory, cores, console, pwd, background, max_memory) if background: gandi.pretty_echo(result) return result @vm.command() @click.argument('resource') @pass_gandi def console(gandi, resource): """Open a console to virtual machine. Resource can be a Hostname or an ID """ gandi.echo('/!\ Please be aware that if you didn\'t provide a password ' 'during creation, console service will be unavailable.') gandi.echo('/!\ You can use "gandi vm update" command to set a password.') gandi.echo('/!\ Use ~. ssh escape key to exit.') gandi.iaas.console(resource) @vm.command() @click.option('--wait', default=False, is_flag=True, help='Wait for virtual machine sshd to come up (timeout 2min).') @click.option('--wipe-key', default=False, is_flag=True, help='Wipe SSH known host entry first.') @click.option('--login', '-l', default='root', help='Use given login for ssh call') @click.option('--identity', '-i', default=None, help='Use specified path for ssh key') @click.argument('resource') @click.argument('args', nargs=-1) @pass_gandi def ssh(gandi, resource, login, identity, wipe_key, wait, args): """Spawn an SSH session to virtual machine. Resource can be a Hostname or an ID """ if '@' in resource: (login, resource) = resource.split('@', 1) if wipe_key: gandi.iaas.ssh_keyscan(resource) if wait: gandi.iaas.wait_for_sshd(resource) gandi.iaas.ssh(resource, login, identity, args) @vm.command() @click.option('--datacenter', type=DATACENTER, default=None, help='Filter by datacenter.') @click.argument('label', required=False) @pass_gandi def images(gandi, label, datacenter): """List available system images for virtual machines. You can also filter results using label, by example: $ gandi vm images Ubuntu --datacenter LU or $ gandi vm images 'Ubuntu 10.04' --datacenter LU """ output_keys = ['label', 'os_arch', 'kernel_version', 'disk_id', 'dc', 'name'] datacenters = gandi.datacenter.list() result = gandi.image.list(datacenter, label) for num, image in enumerate(result): if num: gandi.separator_line() output_image(gandi, image, datacenters, output_keys) # also display usable disks result = gandi.disk.list_create(datacenter, label) for disk in result: gandi.separator_line() output_image(gandi, disk, datacenters, output_keys) return result @vm.command() @click.option('--vm', default=None, help='Output available kernels for given vm.') @click.option('--datacenter', type=DATACENTER, default=None, help='Filter by datacenter.') @click.option('--flavor', default=None, help='Filter by kernel flavor.') @click.argument('match', default='', required=False, metavar='pattern') @pass_gandi def kernels(gandi, vm, datacenter, flavor, match): """List available kernels.""" if vm: vm = gandi.iaas.info(vm) dc_list = gandi.datacenter.filtered_list(datacenter, vm) for num, dc in enumerate(dc_list): if num: gandi.echo('\n') output_datacenter(gandi, dc, ['dc_name']) kmap = gandi.kernel.list(dc['id'], flavor, match) for _flavor in kmap: gandi.separator_line() output_kernels(gandi, _flavor, kmap[_flavor]) @cli.command() @click.option('--id', help='Display ids.', is_flag=True) @pass_gandi def datacenters(gandi, id): """List available datacenters.""" output_keys = ['iso', 'name', 'country', 'dc_code', 'status'] if id: output_keys.append('id') result = gandi.datacenter.list() for num, dc in enumerate(result): if num: gandi.separator_line() output_datacenter(gandi, dc, output_keys, justify=10) return result @vm.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.option('--finalize', default=False, is_flag=True, help='Finalize migration.') @click.argument('resource', required=True) @pass_gandi def migrate(gandi, resource, force, background, finalize): """ Migrate a virtual machine to another datacenter. """ if not gandi.iaas.check_can_migrate(resource): return if not force: proceed = click.confirm('Are you sure you want to migrate VM %s ?' % resource) if not proceed: return if finalize: gandi.iaas.need_finalize(resource) output_keys = ['id', 'type', 'step'] oper = gandi.iaas.migrate(resource, background, finalize=finalize) if background: output_generic(gandi, oper, output_keys) return oper gandi.cli-1.2/gandi/cli/commands/record.py0000644000175000017500000001727413227414205021357 0ustar sayounsayoun00000000000000""" Record namespace commands. """ import os import click import json from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_generic, ) from gandi.cli.core.params import pass_gandi, StringConstraint @cli.group(name='record') @pass_gandi def record(gandi): """Commands related to DNS zone records (xmlrpc).""" @record.command() @click.option('--zone-id', '-z', default=None, type=click.INT, help='Zone ID to use, if not set default zone will be used.') @click.option('--output', '-o', is_flag=True, help='Write the records into a file.') @click.option('--format', '-f', type=click.Choice(['text', 'json']), help='Choose the output format', required=False) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @click.argument('domain', required=True) @pass_gandi def list(gandi, domain, zone_id, output, format, limit): """List DNS zone records for a domain.""" options = { 'items_per_page': limit, } output_keys = ['name', 'type', 'value', 'ttl'] if not zone_id: result = gandi.domain.info(domain) zone_id = result['zone_id'] if not zone_id: gandi.echo('No zone records found, domain %s doesn\'t seems to be ' 'managed at Gandi.' % domain) return records = gandi.record.list(zone_id, options) if not output and not format: for num, rec in enumerate(records): if num: gandi.separator_line() output_generic(gandi, rec, output_keys, justify=12) elif output: zone_filename = domain + "_" + str(zone_id) if os.path.isfile(zone_filename): open(zone_filename, 'w').close() for record in records: format_record = ('%s %s IN %s %s' % (record['name'], record['ttl'], record['type'], record['value'])) with open(zone_filename, 'ab') as zone_file: zone_file.write(format_record + '\n') gandi.echo('Your zone file have been writen in %s' % zone_filename) elif format: if format == 'text': for record in records: format_record = ('%s %s IN %s %s' % (record['name'], record['ttl'], record['type'], record['value'])) gandi.echo(format_record) if format == 'json': format_record = json.dumps(records, sort_keys=True, indent=4, separators=(',', ': ')) gandi.echo(format_record) return records @record.command() @click.option('--zone-id', '-z', default=None, type=click.INT, help='Zone ID to use, if not set, default zone will be used.') @click.option('--name', default=None, required=True, help='Relative name, may contain leading wildcard. ' '`@` for empty name') @click.option('--type', default=None, required=True, type=click.Choice(['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'WKS', 'SRV', 'LOC', 'SPF']), help='DNS record type') @click.option('--value', default=None, required=True, type=StringConstraint(minlen=1, maxlen=1024), help='Value for record. Semantics depends on the record type.' 'Currently limited to 1024 ASCII characters.' 'In case of TXT, each part between quotes is limited to 255' ' characters') @click.option('--ttl', default=None, required=False, type=click.IntRange(min=300, max=2592000), help='Time to live, in seconds, between 5 minutes and 30 days') @click.argument('domain', required=True) @pass_gandi def create(gandi, domain, zone_id, name, type, value, ttl): """Create new DNS zone record entry for a domain.""" if not zone_id: result = gandi.domain.info(domain) zone_id = result['zone_id'] if not zone_id: gandi.echo('No zone records found, domain %s doesn\'t seems to be ' 'managed at Gandi.' % domain) return record = {'type': type, 'name': name, 'value': value} if ttl: record['ttl'] = ttl result = gandi.record.create(zone_id, record) return result @record.command() @click.option('--zone-id', '-z', default=None, type=click.INT, help='Zone ID to use, if not set, default zone will be used.') @click.option('--name', default=None, help='Relative name of the record to delete.') @click.option('--type', default=None, type=click.Choice(['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'WKS', 'SRV', 'LOC', 'SPF']), help='DNS record type') @click.option('--value', default=None, type=StringConstraint(minlen=1, maxlen=1024), help='Value for record. Semantics depends on the record type.' 'Currently limited to 1024 ASCII characters.' 'In case of TXT, each part between quotes is limited to 255' ' characters') @click.argument('domain', required=True) @pass_gandi def delete(gandi, domain, zone_id, name, type, value): """Delete a record entry for a domain""" if not zone_id: result = gandi.domain.info(domain) zone_id = result['zone_id'] if not zone_id: gandi.echo('No zone records found, domain %s doesn\'t seems to be ' 'managed at Gandi.' % domain) return if not name and not type and not value: proceed = click.confirm('This command without parameters --type, ' '--name or --value will remove all records' ' in this zone file. Are you sur to ' 'perform this action ?') if not proceed: return record = {'name': name, 'type': type, 'value': value} result = gandi.record.delete(zone_id, record) return result @record.command() @click.option('--zone-id', '-z', default=None, type=click.INT, help='Zone ID to use, if not set, default zone will be used.') @click.option('--file', '-f', type=click.File('r'), required=False, help='Filename of the zone file.') @click.option('--record', '-r', default=None, required=False, help="'name TTL IN TYPE [A, AAAA, MX, TXT, SPF] value' \n" "Note that you can specify only a name, but in case of " "multiple entries with the same name, only the first one will " "be updated") @click.option('--new-record', default=None, required=False, help="'name TTL IN TYPE [A, AAAA, MX, TXT, SPF] value'") @click.argument('domain', required=True) @pass_gandi def update(gandi, domain, zone_id, file, record, new_record): """Update records entries for a domain. You can update an individual record using --record and --new-record parameters Or you can use a plaintext file to update all records of a DNS zone at once with --file parameter. """ if not zone_id: result = gandi.domain.info(domain) zone_id = result['zone_id'] if not zone_id: gandi.echo('No zone records found, domain %s doesn\'t seems to be' ' managed at Gandi.' % domain) return if file: records = file.read() result = gandi.record.zone_update(zone_id, records) return result elif record and new_record: result = gandi.record.update(zone_id, record, new_record) return result else: gandi.echo('You must indicate a zone file or a record.' ' Use `gandi record update --help` for more information') gandi.cli-1.2/gandi/cli/commands/oper.py0000644000175000017500000000244313164644514021046 0ustar sayounsayoun00000000000000""" Operation namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_generic from gandi.cli.core.params import pass_gandi, OPER_STEP @cli.group(name='oper') @pass_gandi def oper(gandi): """Commands related to Gandi operations.""" @oper.command() @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @click.option('--step', '-s', type=OPER_STEP, help='Filter the result by step', default=['BILL', 'WAIT', 'RUN'], multiple=True, show_default=True) @pass_gandi def list(gandi, limit, step): """List operations.""" output_keys = ['id', 'type', 'step'] options = { 'step': step, 'items_per_page': limit, 'sort_by': 'date_created DESC' } result = gandi.oper.list(options) for num, oper in enumerate(reversed(result)): if num: gandi.separator_line() output_generic(gandi, oper, output_keys) return result @oper.command() @click.argument('id', type=click.INT) @pass_gandi def info(gandi, id): """Display information about an operation.""" output_keys = ['id', 'type', 'step', 'last_error'] oper = gandi.oper.info(id) output_generic(gandi, oper, output_keys) return oper gandi.cli-1.2/gandi/cli/commands/root.py0000644000175000017500000000573213155727500021065 0ustar sayounsayoun00000000000000""" Main namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_generic, output_service from gandi.cli.core.params import pass_gandi @cli.command() @pass_gandi def setup(gandi): """ Initialize Gandi CLI configuration. Create global configuration directory with API credentials """ intro = """Welcome to GandiCLI, let's configure a few things before we \ start. """ outro = """ Setup completed. You can now: * use 'gandi' to see all command. * use 'gandi vm create' to create and access a Virtual Machine. * use 'gandi paas create' to create and access a SimpleHosting instance. """ gandi.echo(intro) gandi.init_config() gandi.echo(outro) @cli.command() @pass_gandi def api(gandi): """Display information about API used.""" key_name = 'API version' result = gandi.api.info() result[key_name] = result.pop('api_version') output_generic(gandi, result, [key_name]) return result @cli.command() @click.argument('command', required=False, nargs=-1) @click.pass_context def help(ctx, command): """Display help for a command.""" command = ' '.join(command) if not command: click.echo(cli.get_help(ctx)) return cmd = cli.get_command(ctx, command) if cmd: click.echo(cmd.get_help(ctx)) else: click.echo(cli.get_help(ctx)) @cli.command() @click.argument('service', required=False) @pass_gandi def status(gandi, service): """Display current status from status.gandi.net.""" if not service: global_status = gandi.status.status() if global_status['status'] == 'FOGGY': # something is going on but not affecting services filters = { 'category': 'Incident', 'current': True, } events = gandi.status.events(filters) for event in events: if event['services']: # do not process services continue event_url = gandi.status.event_timeline(event) service_detail = '%s - %s' % (event['title'], event_url) gandi.echo(service_detail) # then check other services descs = gandi.status.descriptions() needed = services = gandi.status.services() if service: needed = [serv for serv in services if serv['name'].lower() == service.lower()] for serv in needed: if serv['status'] != 'STORMY': output_service(gandi, serv['name'], descs[serv['status']]) continue filters = { 'category': 'Incident', 'services': serv['name'], 'current': True, } events = gandi.status.events(filters) for event in events: event_url = gandi.status.event_timeline(event) service_detail = '%s - %s' % (event['title'], event_url) output_service(gandi, serv['name'], service_detail) return services gandi.cli-1.2/gandi/cli/commands/domain.py0000644000175000017500000000676413164644514021362 0ustar sayounsayoun00000000000000""" Domain namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_contact_info, output_domain from gandi.cli.core.params import pass_gandi @cli.group(name='domain') @pass_gandi def domain(gandi): """Commands related to domains.""" @domain.command() @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, limit): """List domains.""" options = {'items_per_page': limit} domains = gandi.domain.list(options) for domain in domains: gandi.echo(domain['fqdn']) return domains @domain.command() @click.argument('resource') @pass_gandi def info(gandi, resource): """Display information about a domain.""" output_keys = ['fqdn', 'nameservers', 'services', 'zone_id', 'tags', 'created', 'expires', 'updated'] contact_field = ['owner', 'admin', 'bill', 'tech', 'reseller'] result = gandi.domain.info(resource) output_contact_info(gandi, result['contacts'], contact_field, justify=12) output_domain(gandi, result, output_keys, justify=12) return result @domain.command() @click.option('--domain', default=None, help='Name of the domain.') @click.option('--duration', default=1, prompt=True, type=click.IntRange(min=1, max=10), help='Registration period in years, between 1 and 10.') @click.option('--owner', default=None, help='Registrant handle.') @click.option('--admin', default=None, help='Administrative contact handle.') @click.option('--tech', default=None, help='Technical contact handle.') @click.option('--bill', default=None, help='Billing contact handle.') @click.option('--nameserver', default=None, help='Nameserver', multiple=True) @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', metavar='DOMAIN', required=False) @pass_gandi def create(gandi, resource, domain, duration, owner, admin, tech, bill, nameserver, background): """Buy a domain.""" if domain: gandi.echo('/!\ --domain option is deprecated and will be removed ' 'upon next release.') gandi.echo("You should use 'gandi domain create %s' instead." % domain) if (domain and resource) and (domain != resource): gandi.echo('/!\ You specified both an option and an argument which ' 'are different, please choose only one between: %s and %s.' % (domain, resource)) return _domain = domain or resource if not _domain: _domain = click.prompt('Name of the domain') result = gandi.domain.create(_domain, duration, owner, admin, tech, bill, nameserver, background) if background: gandi.pretty_echo(result) return result @domain.command() @click.option('--duration', default=1, prompt=True, type=click.IntRange(min=1, max=10), help='Registration period in years, between 1 and 10.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('domain') @pass_gandi def renew(gandi, domain, duration, background): """Renew a domain.""" result = gandi.domain.renew(domain, duration, background) if background: gandi.pretty_echo(result) return result gandi.cli-1.2/gandi/cli/commands/forward.py0000644000175000017500000000502013164644514021537 0ustar sayounsayoun00000000000000""" Forward namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_forward from gandi.cli.core.params import pass_gandi, EMAIL_TYPE @cli.group(name='forward') @pass_gandi def forward(gandi): """Commands related to domain mail forwards.""" @forward.command() @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @click.argument('domain', metavar='domain.tld') @pass_gandi def list(gandi, domain, limit): """List mail forwards for a domain.""" options = {'items_per_page': limit} result = gandi.forward.list(domain, options) for forward in result: output_forward(gandi, domain, forward) return result @forward.command() @click.option('--destination', '-d', help='Add forward destination.', multiple=True, required=True) @click.argument('address', type=EMAIL_TYPE, metavar='address@domain.tld') @pass_gandi def create(gandi, address, destination): """Create a domain mail forward.""" source, domain = address result = gandi.forward.create(domain, source, destination) return result @forward.command() @click.option('--dest-add', '-a', help='Add forward destination.', multiple=True, required=False) @click.option('--dest-del', '-d', help='Delete forward destination.', multiple=True, required=False) @click.argument('address', type=EMAIL_TYPE, metavar='address@domain.tld') @pass_gandi def update(gandi, address, dest_add, dest_del): """Update a domain mail forward.""" source, domain = address if not dest_add and not dest_del: gandi.echo('Nothing to update: you must provide destinations to ' 'update, use --dest-add/-a or -dest-del/-d parameters.') return result = gandi.forward.update(domain, source, dest_add, dest_del) return result @forward.command() @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('address', type=EMAIL_TYPE, metavar='address@domain.tld') @pass_gandi def delete(gandi, address, force): """Delete a domain mail forward.""" source, domain = address if not force: proceed = click.confirm('Are you sure to delete the domain ' 'mail forward %s@%s ?' % (source, domain)) if not proceed: return result = gandi.forward.delete(domain, source) return result gandi.cli-1.2/gandi/cli/commands/contact.py0000644000175000017500000000764413164644514021544 0ustar sayounsayoun00000000000000""" Contact namespace commands. """ import click import time import webbrowser # define unicode for python3 try: unicode except NameError: unicode = str from gandi.cli.core.cli import cli from gandi.cli.core.utils import randomstring from gandi.cli.core.params import pass_gandi FIELDS = (('type', 'Choose your contact type', {'valid': ((0, 'individual'), (1, 'company'), (2, 'association'), (3, 'public body'), (4, 'reseller')), 'convert': int}), ('given', 'What is your first name', None), ('family', 'What is your last name', None), ('orgname', 'What is your company name', {'display': lambda contact_: contact_['type'] != 0}), ('email', 'What is your email address', None), ('streetaddr', 'What is your street address', None), ('zip', 'What is your zipcode', None), ('city', 'Which city', None), ('country', 'Which country', None), ('phone', 'What is your telephone number', None)) FIELDS_POSITION = dict([(j, i) for i, j in enumerate( [field[0] for field in FIELDS])]) def ask_field(gandi, contact, field, label, checks): valid = display = None convert = unicode if checks: valid = checks.get('valid') convert = checks.get('convert', unicode) display = checks.get('display') if display and not display(contact): return if not valid: contact[field] = convert(click.prompt(label)) elif isinstance(valid, (tuple, list)): valid_keys = [unicode(val) for val, _ in valid] value = None gandi.echo(label) while value not in valid_keys: for key, val in valid: gandi.echo('%s- %s' % (key, val)) value = click.prompt('') contact[field] = convert(value) @cli.group(name='contact') @pass_gandi def contact(gandi): """Commands related to contacts.""" @contact.command() @pass_gandi def create(gandi): """ Create a new contact. """ contact = {} for field, label, checks in FIELDS: ask_field(gandi, contact, field, label, checks) default_pwd = randomstring(16) contact['password'] = click.prompt('Please enter your password', hide_input=True, confirmation_prompt=True, default=default_pwd) result = True while result: result = gandi.contact.create_dry_run(contact) # display errors for err in result: gandi.echo(err['reason']) field = err['field'] if field not in FIELDS_POSITION: return desc = FIELDS[FIELDS_POSITION.get(field)] ask_field(gandi, contact, *desc) result = gandi.contact.create(contact) handle = result['handle'] gandi.echo('Please activate you public api access from gandi website, and ' 'get the apikey.') gandi.echo('Your handle is %s, and the password is the one you defined.' % handle) # open new browser window webbrowser.open('https://www.gandi.net/admin/api_key') # just to avoid missing the next question in webbrowser stderr time.sleep(1) # get the apikey from shell apikey = None while not apikey: apikey = click.prompt('What is your production apikey') caller = gandi.get('api.key') # save apikey in the conf if none defined else display an help on how to # use it if caller: gandi.echo('You already have an apikey defined, if you want to use the' ' newly created contact, use the env var : ') gandi.echo('export API_KEY=%s' % apikey) else: gandi.echo('Will save your apikey into the config file.') gandi.configure(True, 'api.key', apikey) return handle gandi.cli-1.2/gandi/cli/commands/certificate.py0000644000175000017500000004402613164644514022366 0ustar sayounsayoun00000000000000""" Certificate namespace commands. """ import os import click import requests # define basestring for python3 try: basestring except NameError: basestring = (str, bytes) type_list = list from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_cert, output_cert_oper, display_rows from gandi.cli.core.params import (pass_gandi, IntChoice, CERTIFICATE_PACKAGE, CERTIFICATE_DCV_METHOD, CERTIFICATE_PACKAGE_TYPE, CERTIFICATE_PACKAGE_MAX, CERTIFICATE_PACKAGE_WARRANTY) @cli.group(name='certificate') @pass_gandi def certificate(gandi): """Commands related to certificates.""" @certificate.command() @pass_gandi def packages(gandi): """ List certificate packages. /!\\ deprecated call. """ gandi.echo('/!\ "gandi certificate packages" is deprecated.') gandi.echo('Please use "gandi certificate plans".') return _plans(gandi, with_name=True) @certificate.command() @pass_gandi def plans(gandi): """ List certificate plans. """ return _plans(gandi) def package_desc(gandi, package): if isinstance(package, basestring): package = gandi.certificate.package_get(package) if not package: return '' type_ = package['category']['name'] if package['wildcard']: desc = '%s wildcard' % type_ elif package['max_domains'] > 1: desc = '%s multi domain' % type_ else: desc = '%s single domain' % type_ return ' '.join([word.capitalize() for word in desc.split(' ')]) def _plans(gandi, with_name=False): packages = gandi.certificate.package_list() def keyfunc(item): return (item['category']['id'], item['max_domains'], item['warranty'], item['name']) packages.sort(key=keyfunc) labels = ['Description', 'Max altnames', 'Type'] if with_name: labels.insert(1, 'Name') else: labels.append('Warranty') ret = [labels] for package in packages: params = package['name'].split('_') cat = package['name'].split('_')[1] warranty = str(int(package['name'].split('_')[3]) * 1000) if len(warranty) > 3: warranty = type_list(warranty) warranty.insert(len(warranty) - 3, ',') warranty = ''.join(warranty) desc = package_desc(gandi, package) line = [desc, str(package['max_domains']), cat] if with_name: line.insert(1, package['name']) else: line.append(warranty) ret.append(line) display_rows(gandi, ret) return ret @certificate.command() @click.option('--id', help='Display ids.', is_flag=True) @click.option('--altnames', help='Display altnames.', is_flag=True) @click.option('--csr', help='Display CSR.', is_flag=True) @click.option('--cert', help='Display CRT.', is_flag=True) @click.option('--all-status', is_flag=True, help='Filter the certificate without regard to its status.') @click.option('--status', help='Display status.', is_flag=True) @click.option('--dates', help='Display dates.', is_flag=True) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, id, altnames, csr, cert, all_status, status, dates, limit): """ List certificates. """ options = {'items_per_page': limit} if not all_status: options['status'] = ['valid', 'pending'] output_keys = ['cn', 'plan'] if id: output_keys.append('id') if status: output_keys.append('status') if dates: output_keys.extend(['date_created', 'date_end']) if altnames: output_keys.append('altnames') if csr: output_keys.append('csr') if cert: output_keys.append('cert') result = gandi.certificate.list(options) for num, cert in enumerate(result): if num: gandi.separator_line() cert['plan'] = package_desc(gandi, cert['package']) output_cert(gandi, cert, output_keys) return result @certificate.command() @click.argument('resource', nargs=-1, required=True) @click.option('--id', help='Display ids.', is_flag=True) @click.option('--altnames', help='Display altnames.', is_flag=True) @click.option('--csr', help='Display CSR.', is_flag=True) @click.option('--cert', help='Display CRT.', is_flag=True) @click.option('--all-status', help='Show all certificates.', is_flag=True) @pass_gandi def info(gandi, resource, id, altnames, csr, cert, all_status): """ Display information about a certificate. Resource can be a CN or an ID """ output_keys = ['cn', 'date_created', 'date_end', 'plan', 'status'] if id: output_keys.append('id') if altnames: output_keys.append('altnames') if csr: output_keys.append('csr') if cert: output_keys.append('cert') ids = [] for res in resource: ids.extend(gandi.certificate.usable_ids(res)) result = [] for num, id_ in enumerate(set(ids)): cert = gandi.certificate.info(id_) if not all_status and cert['status'] not in ['valid', 'pending']: continue if num: gandi.separator_line() cert['plan'] = package_desc(gandi, cert['package']) output_cert(gandi, cert, output_keys) result.append(cert) return result @certificate.command() @click.argument('resource', nargs=-1, required=True) @click.option('-o', '--output', help='The file to write the cert.') @click.option('--force', '-f', is_flag=True, help='Overwrite the crt file if it exists.') @click.option('-i', '--intermediate', is_flag=True, help='Retrieve gandi intermediate certs.') @pass_gandi def export(gandi, resource, output, force, intermediate): """ Write the certificate to or .crt. Resource can be a CN or an ID """ ids = [] for res in resource: ids.extend(gandi.certificate.usable_ids(res)) if output and len(ids) > 1: gandi.echo('Too many certs found, you must specify which cert you ' 'want to export') return for id_ in set(ids): cert = gandi.certificate.info(id_) if 'cert' not in cert: continue if cert['status'] != 'valid': gandi.echo('The certificate must be in valid status to be ' 'exported (%s).' % id_) continue cert_filename = cert['cn'].replace('*.', 'wildcard.', 1) crt_filename = output or cert_filename + '.crt' if not force and os.path.isfile(crt_filename): gandi.echo('The file %s already exists.' % crt_filename) continue crt = gandi.certificate.pretty_format_cert(cert) if crt: with open(crt_filename, 'w') as crt_file: crt_file.write(crt) gandi.echo('wrote %s' % crt_filename) package = cert['package'] if 'bus' in package and intermediate: gandi.echo('Business certs do not need intermediates.') elif intermediate: crtf = 'pem' sha_version = cert['sha_version'] type_ = package.split('_')[1] extra = ('sgc' if 'SGC' in package and 'pro' in package and sha_version == 1 else 'default') if extra == 'sgc': crtf = 'pem' inters = gandi.certificate.urls[sha_version][type_][extra][crtf] if isinstance(inters, basestring): inters = [inters] fhandle = open(cert_filename + '.inter.crt', 'w+b') for inter in inters: if inter.startswith('http'): data = requests.get(inter).text else: data = inter fhandle.write(data.encode('latin1')) gandi.echo('wrote %s' % cert_filename + '.inter.crt') fhandle.close() return crt @certificate.command() @click.option('--csr', required=False, help='Csr of the new certificate (filename or content).') @click.option('--pk', '--private-key', required=False, help='Private key to use to generate the CSR (filename or ' 'content).') @click.option('--cn', '--common-name', required=False, help='Common name to use when generating the CSR.') @click.option('--country', required=False, help='The generated CSR country (C).') @click.option('--state', required=False, help='The generated CSR state (ST).') @click.option('--city', required=False, help='The generated CSR location (L).') @click.option('--organisation', required=False, help='The generated CSR organisation (O).') @click.option('--branch', required=False, help='The generated CSR branch (OU).') @click.option('-d', '--duration', default=1, type=IntChoice(['1', '2']), help='The certificate duration in year.') @click.option('--package', type=CERTIFICATE_PACKAGE, help='Certificate package.') @click.option('--type', type=CERTIFICATE_PACKAGE_TYPE, help='Certificate package type (default=std).') @click.option('--max-altname', type=CERTIFICATE_PACKAGE_MAX, help='Certificate package max altname number.') @click.option('--warranty', type=CERTIFICATE_PACKAGE_WARRANTY, help='Certificate warranty, only good for pro certificates.') @click.option('--altnames', required=False, multiple=True, help='The certificate altnames (comma separated text without ' 'space).') @click.option('--dcv-method', required=False, type=CERTIFICATE_DCV_METHOD, help='Give the DCV method to use to check domain ownership.') @pass_gandi def create(gandi, csr, private_key, common_name, country, state, city, organisation, branch, duration, package, type, max_altname, warranty, altnames, dcv_method): """Create a new certificate.""" if not (csr or common_name): gandi.echo('You need a CSR or a CN to create a certificate.') return if package and (type or max_altname or warranty): gandi.echo('Please do not use --package at the same time you use ' '--type, --max-altname or --warranty.') return if type and warranty and type != 'pro': gandi.echo('The warranty can only be specified for pro certificates.') return csr = gandi.certificate.process_csr(common_name, csr, private_key, country, state, city, organisation, branch) if not csr: return if not common_name: common_name = gandi.certificate.get_common_name(csr) if not common_name: gandi.echo('Unable to parse provided csr: %s' % csr) return if '*' in common_name and altnames and len(altnames) > 1: gandi.echo("You can't have a wildcard with multidomain certificate.") return if package: gandi.echo('/!\ Using --package is deprecated, please replace it by ' '--type (in std, pro or bus) and --max-altname to set ' 'the max number of altnames.') elif type or max_altname or warranty: package = gandi.certificate.get_package(common_name, type, max_altname, altnames, warranty) if not package: gandi.echo("Can't find any plan with your params.") gandi.echo('Please call : "gandi certificate plans".') return result = gandi.certificate.create(csr, duration, package, altnames, dcv_method) gandi.echo('The certificate create operation is %s' % result['id']) gandi.echo('You can follow it with:') gandi.echo('$ gandi certificate follow %s' % result['id']) if common_name: gandi.echo('When the operation is DONE, you can retrieve the .crt' ' with:') gandi.echo('$ gandi certificate export "%s"' % common_name) return result @certificate.command() @click.argument('resource', nargs=1, required=True) @click.option('--csr', help='New csr for the certificate.', required=False) @click.option('--pk', '--private-key', required=False, help='Private key to use to generate the CSR.') @click.option('--c', '--country', required=False, help='The generated CSR country (C).') @click.option('--st', '--state', required=False, help='The generated CSR state (ST).') @click.option('--l', '--city', required=False, help='The generated CSR location (L).') @click.option('--o', '--organisation', required=False, help='The generated CSR organisation (O).') @click.option('--ou', '--branch', required=False, help='The generated CSR branch (OU).') @click.option('--altnames', required=False, multiple=True, help='All the certificate altnames (comma separated text ' 'without space).') @click.option('--dcv-method', required=False, type=CERTIFICATE_DCV_METHOD, help='Give the DCV method to use to check domain ownership.') @pass_gandi def update(gandi, resource, csr, private_key, country, state, city, organisation, branch, altnames, dcv_method): """ Update a certificate CSR. Resource can be a CN or an ID """ ids = gandi.certificate.usable_ids(resource) if len(ids) > 1: gandi.echo('Will not update, %s is not precise enough.' % resource) gandi.echo(' * cert : ' + '\n * cert : '.join([str(id_) for id_ in ids])) return id_ = ids[0] result = gandi.certificate.update(id_, csr, private_key, country, state, city, organisation, branch, altnames, dcv_method) gandi.echo('The certificate update operation is %s' % result['id']) gandi.echo('You can follow it with:') gandi.echo('$ gandi certificate follow %s' % result['id']) gandi.echo('When the operation is DONE, you can retrieve the .crt' ' with:') gandi.echo('$ gandi certificate export "%s"' % resource) return result @certificate.command() @click.argument('resource', nargs=1, required=True) @pass_gandi def follow(gandi, resource): """ Get the operation status Resource is an operation ID """ oper = gandi.oper.info(int(resource)) assert(oper['type'].startswith('certificate_')) output_cert_oper(gandi, oper) return oper @certificate.command('change-dcv') @click.argument('resource', nargs=1, required=True) @click.option('--dcv-method', required=True, type=CERTIFICATE_DCV_METHOD, help='Give the updated DCV method to use.') @pass_gandi def change_dcv(gandi, resource, dcv_method): """ Change the DCV for a running certificate operation. Resource can be a CN or an ID """ ids = gandi.certificate.usable_ids(resource) if len(ids) > 1: gandi.echo('Will not update, %s is not precise enough.' % resource) gandi.echo(' * cert : ' + '\n * cert : '.join([str(id_) for id_ in ids])) return id_ = ids[0] opers = gandi.oper.list({'cert_id': id_}) if not opers: gandi.echo('Can not find any operation for this certificate.') return oper = opers[0] if (oper['step'] != 'RUN' and oper['params']['inner_step'] != 'comodo_oper_updated'): gandi.echo('This certificate operation is not in the good step to ' 'update the DCV method.') return gandi.certificate.change_dcv(oper['id'], dcv_method) cert = gandi.certificate.info(id_) csr = oper['params']['csr'] package = cert['package'] altnames = oper['params'].get('altnames') gandi.certificate.advice_dcv_method(csr, package, altnames, dcv_method) @certificate.command('resend-dcv') @click.argument('resource', nargs=1, required=True) @pass_gandi def resend_dcv(gandi, resource): """ Resend the DCV mail. Resource can be a CN or an ID """ ids = gandi.certificate.usable_ids(resource) if len(ids) > 1: gandi.echo('Will not update, %s is not precise enough.' % resource) gandi.echo(' * cert : ' + '\n * cert : '.join([str(id_) for id_ in ids])) return id_ = ids[0] opers = gandi.oper.list({'cert_id': id_}) if not opers: gandi.echo('Can not find any operation for this certificate.') return oper = opers[0] if (oper['step'] != 'RUN' and oper['params']['inner_step'] != 'comodo_oper_updated'): gandi.echo('This certificate operation is not in the good step to ' 'resend the DCV.') return if oper['params']['dcv_method'] != 'email': gandi.echo('This certificate operation is not in email DCV.') return gandi.certificate.resend_dcv(oper['id']) @certificate.command() @click.argument('resource', nargs=1, required=True) @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def delete(gandi, resource, background, force): """ Revoke the certificate. Resource can be a CN or an ID """ ids = gandi.certificate.usable_ids(resource) if len(ids) > 1: gandi.echo('Will not delete, %s is not precise enough.' % resource) gandi.echo(' * cert : ' + '\n * cert : '.join([str(id_) for id_ in ids])) return if not force: proceed = click.confirm("Are you sure to delete the certificate %s?" % resource) if not proceed: return result = gandi.certificate.delete(ids[0], background) return result gandi.cli-1.2/gandi/cli/commands/dns.py0000644000175000017500000001774513227142762020676 0ustar sayounsayoun00000000000000""" DNS namespace commands. """ import sys import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_dns_records, output_generic from gandi.cli.core.params import pass_gandi, DNS_RECORDS @cli.group(name='dns') @pass_gandi def dns(gandi): """Commands related to LiveDNS.""" @dns.command('domain.list') @pass_gandi def domain_list(gandi): """List domains manageable by REST API.""" domains = gandi.dns.list() for domain in domains: gandi.echo(domain['fqdn']) return domains @dns.command() @click.argument('fqdn') @click.argument('name', default=None, required=False) @click.argument('rrset_type', default=None, required=False, type=DNS_RECORDS) @click.option('--sort', default='name', type=click.Choice(['name', 'ttl', 'type', 'values']), help='Sort results (does not work with text option).') @click.option('--type', default=None, type=DNS_RECORDS, help='Filter results by type (does not work with text option).') @click.option('--text', default=False, is_flag=True, help='Output result as text.') @pass_gandi def list(gandi, fqdn, name, sort, type, rrset_type, text): """Display records for a domain.""" domains = gandi.dns.list() domains = [domain['fqdn'] for domain in domains] if fqdn not in domains: gandi.echo('Sorry domain %s does not exist' % fqdn) gandi.echo('Please use one of the following: %s' % ', '.join(domains)) return output_keys = ['name', 'ttl', 'type', 'values'] result = gandi.dns.records(fqdn, sort_by=sort, text=text) if text: gandi.echo(result) return result for num, rec in enumerate(result): if type and rec['rrset_type'] != type: continue if rrset_type and rec['rrset_type'] != rrset_type: continue if name and rec['rrset_name'] != name: continue if num: gandi.separator_line() output_dns_records(gandi, rec, output_keys) return result @dns.command() @click.argument('fqdn') @click.argument('name') @click.argument('type', type=DNS_RECORDS) @click.argument('value', nargs=-1, required=True) @click.option('--ttl', default=None, type=click.INT, required=False, help='Time to live, in seconds') @pass_gandi def create(gandi, fqdn, name, type, value, ttl): """Create new record entry for a domain. multiple value parameters can be provided. """ domains = gandi.dns.list() domains = [domain['fqdn'] for domain in domains] if fqdn not in domains: gandi.echo('Sorry domain %s does not exist' % fqdn) gandi.echo('Please use one of the following: %s' % ', '.join(domains)) return result = gandi.dns.add_record(fqdn, name, type, value, ttl) gandi.echo(result['message']) @dns.command() @click.argument('fqdn') @click.argument('name', required=False) @click.argument('type', type=DNS_RECORDS, required=False) @click.argument('value', nargs=-1, required=False) @click.option('--ttl', default=None, type=click.INT, required=False, help='Time to live, in seconds') @click.option('-f', '--file', type=click.File('rb'), help='Zone content in a plain text file. If provided this will ' 'ignore all other parameters and overwrite current zone ' 'content') @pass_gandi def update(gandi, fqdn, name, type, value, ttl, file): """Update record entry for a domain. --file option will ignore other parameters and overwrite current zone content with provided file content. """ domains = gandi.dns.list() domains = [domain['fqdn'] for domain in domains] if fqdn not in domains: gandi.echo('Sorry domain %s does not exist' % fqdn) gandi.echo('Please use one of the following: %s' % ', '.join(domains)) return content = '' if file: content = file.read() elif not sys.stdin.isatty(): content = click.get_text_stream('stdin').read() content = content.strip() if not content and not name and not type and not value: click.echo('Cannot find parameters for zone content to update.') return if name and type and not value: click.echo('You must provide one or more value parameter.') return result = gandi.dns.update_record(fqdn, name, type, value, ttl, content) gandi.echo(result['message']) @dns.command() @click.argument('fqdn') @click.argument('name', required=False) @click.argument('type', type=DNS_RECORDS, required=False) @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def delete(gandi, fqdn, name, type, force): """Delete record entry for a domain.""" domains = gandi.dns.list() domains = [domain['fqdn'] for domain in domains] if fqdn not in domains: gandi.echo('Sorry domain %s does not exist' % fqdn) gandi.echo('Please use one of the following: %s' % ', '.join(domains)) return if not force: if not name and not type: prompt = ("Are you sure to delete all records for domain %s ?" % fqdn) elif name and not type: prompt = ("Are you sure to delete all '%s' name records for " "domain %s ?" % (name, fqdn)) else: prompt = ("Are you sure to delete all '%s' records of type %s " "for domain %s ?" % (name, type, fqdn)) proceed = click.confirm(prompt) if not proceed: return result = gandi.dns.del_record(fqdn, name, type) gandi.echo('Delete successful.') return result @dns.group(name='keys') @pass_gandi def keys(gandi): """Commands related to LiveDNS DNSSEC keys.""" @keys.command(name='list') @click.argument('fqdn') @pass_gandi def keys_list(gandi, fqdn): """List domain keys.""" keys = gandi.dns.keys(fqdn) output_keys = ['uuid', 'algorithm', 'algorithm_name', 'ds', 'flags', 'status'] for num, key in enumerate(keys): if num: gandi.separator_line() output_generic(gandi, key, output_keys, justify=15) return keys @keys.command(name='info') @click.argument('fqdn') @click.argument('key') @pass_gandi def keys_info(gandi, fqdn, key): """Display information about a domain key.""" key_info = gandi.dns.keys_info(fqdn, key) output_keys = ['uuid', 'algorithm', 'algorithm_name', 'ds', 'fingerprint', 'public_key', 'flags', 'tag', 'status'] output_generic(gandi, key_info, output_keys, justify=15) return key_info @keys.command(name='create') @click.argument('fqdn') @click.argument('flag', type=click.Choice(['256', '257'])) @pass_gandi def keys_create(gandi, fqdn, flag): """Create key for a domain.""" key_info = gandi.dns.keys_create(fqdn, int(flag)) output_keys = ['uuid', 'algorithm', 'algorithm_name', 'ds', 'fingerprint', 'public_key', 'flags', 'tag', 'status'] output_generic(gandi, key_info, output_keys, justify=15) return key_info @keys.command(name='delete') @click.argument('fqdn') @click.argument('key') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def keys_delete(gandi, fqdn, key, force): """Delete a key for a domain.""" if not force: proceed = click.confirm('Are you sure you want to delete key %s on ' 'domain %s?' % (key, fqdn)) if not proceed: return result = gandi.dns.keys_delete(fqdn, key) gandi.echo('Delete successful.') return result @keys.command(name='recover') @click.argument('fqdn') @click.argument('key') @pass_gandi def keys_recover(gandi, fqdn, key): """Recover deleted key for a domain.""" result = gandi.dns.keys_recover(fqdn, key) gandi.echo('Recover successful.') return result gandi.cli-1.2/gandi/cli/commands/vlan.py0000644000175000017500000001533713227142745021046 0ustar sayounsayoun00000000000000""" Vlan namespace commands. """ import click from IPy import IP from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_vlan, output_generic, output_iface, output_ip, output_line, DatacenterLimited ) from gandi.cli.core.params import option, pass_gandi, DATACENTER @cli.group(name='vlan') @pass_gandi def vlan(gandi): """Commands related to hosting virtual lan networks.""" @vlan.command() @click.option('--datacenter', type=DATACENTER, default=None, help='Filter by datacenter.') @click.option('--id', help='Display ids.', is_flag=True) @click.option('--subnet', help='Display subnets.', is_flag=True) @click.option('--gateway', help='Display gateway.', is_flag=True) @pass_gandi def list(gandi, datacenter, id, subnet, gateway): """List vlans.""" output_keys = ['name', 'state', 'dc'] if id: output_keys.append('id') if subnet: output_keys.append('subnet') if gateway: output_keys.append('gateway') datacenters = gandi.datacenter.list() vlans = gandi.vlan.list(datacenter) for num, vlan in enumerate(vlans): if num: gandi.separator_line() output_vlan(gandi, vlan, datacenters, output_keys) return vlans @vlan.command() @click.option('--ip', help='Display ips.', is_flag=True) @click.argument('resource') @pass_gandi def info(gandi, resource, ip): """Display information about a vlan.""" output_keys = ['name', 'state', 'dc', 'subnet', 'gateway'] datacenters = gandi.datacenter.list() vlan = gandi.vlan.info(resource) gateway = vlan['gateway'] if not ip: output_vlan(gandi, vlan, datacenters, output_keys, justify=11) return vlan gateway_exists = False vms = dict([(vm_['id'], vm_) for vm_ in gandi.iaas.list()]) ifaces = gandi.vlan.ifaces(resource) for iface in ifaces: for ip in iface['ips']: if gateway == ip['ip']: gateway_exists = True if gateway_exists: vlan.pop('gateway') else: vlan['gateway'] = ("%s don't exists" % gateway if gateway else 'none') output_vlan(gandi, vlan, datacenters, output_keys, justify=11) output_keys = ['vm', 'bandwidth'] for iface in ifaces: gandi.separator_line() output_iface(gandi, iface, datacenters, vms, output_keys, justify=11) for ip in iface['ips']: output_ip(gandi, ip, None, None, None, ['ip']) if gateway == ip['ip']: output_line(gandi, 'gateway', 'true', justify=11) return vlan @vlan.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def delete(gandi, background, force, resource): """Delete a vlan. Resource can be a vlan name or an ID """ output_keys = ['id', 'type', 'step'] possible_resources = gandi.vlan.resource_list() for item in resource: if item not in possible_resources: gandi.echo('Sorry vlan %s does not exist' % item) gandi.echo('Please use one of the following: %s' % possible_resources) return if not force: vlan_info = "'%s'" % ', '.join(resource) proceed = click.confirm('Are you sure to delete vlan %s?' % vlan_info) if not proceed: return opers = gandi.vlan.delete(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @vlan.command() @click.option('--name', required=True, help='Name of the vlan.') @option('--datacenter', type=DATACENTER, default='FR-SD5', help='Datacenter where the vlan will be spawned.') @click.option('--subnet', help='The vlan subnet.') @click.option('--gateway', help='The vlan gateway.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @pass_gandi def create(gandi, name, datacenter, subnet, gateway, background): """ Create a new vlan """ try: gandi.datacenter.is_opened(datacenter, 'iaas') except DatacenterLimited as exc: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) result = gandi.vlan.create(name, datacenter, subnet, gateway, background) if background: gandi.pretty_echo(result) return result @vlan.command() @click.option('--name', help='Name of the vlan.') @click.option('--gateway', help='Gateway of the vlan.') @click.option('--create', default=False, is_flag=True, help='If gateway is a vm and does not have any ip in the good ' 'vlan, we will start by putting an ip in the vlan.') @option('--bandwidth', type=click.INT, default=102400, help="Network bandwidth in kbit/s used to create the VM's ip in this " 'vlan.') @click.argument('resource') @pass_gandi def update(gandi, resource, name, gateway, create, bandwidth): """ Update a vlan ``gateway`` can be a vm name or id, or an ip. """ params = {} if name: params['name'] = name vlan_id = gandi.vlan.usable_id(resource) try: if gateway: IP(gateway) params['gateway'] = gateway except ValueError: vm = gandi.iaas.info(gateway) ips = [ip for sublist in [[ip['ip'] for ip in iface['ips'] if ip['version'] == 4] for iface in vm['ifaces'] if iface['vlan'] and iface['vlan'].get('id') == vlan_id] for ip in sublist] if len(ips) > 1: gandi.echo("This vm has two ips in the vlan, don't know which one" ' to choose (%s)' % (', '.join(ips))) return if not ips and not create: gandi.echo("Can't find '%s' in '%s' vlan" % (gateway, resource)) return if not ips and create: gandi.echo('Will create a new ip in this vlan for vm %s' % gateway) oper = gandi.ip.create('4', vm['datacenter_id'], bandwidth, vm['hostname'], resource) iface_id = oper['iface_id'] iface = gandi.iface.info(iface_id) ips = [ip['ip'] for ip in iface['ips'] if ip['version'] == 4] params['gateway'] = ips[0] result = gandi.vlan.update(resource, params) return result gandi.cli-1.2/gandi/cli/commands/__init__.py0000644000175000017500000000010612441335654021631 0ustar sayounsayoun00000000000000""" Contains CLI commands declaration. One module per namespace. """ gandi.cli-1.2/gandi/cli/commands/certstore.py0000644000175000017500000001051713164644514022114 0ustar sayounsayoun00000000000000""" Hosted certificate namespace commands. """ import os import click # define basestring for python3 try: basestring except NameError: basestring = (str, bytes) type_list = list from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_hostedcert from gandi.cli.core.params import pass_gandi @cli.group(name='certstore') @pass_gandi def certstore(gandi): """Commands related to certificate store.""" @certstore.command() @click.option('--id', help='Display ids.', is_flag=True) @click.option('--vhosts', help='Display related vhosts.', is_flag=True) @click.option('--dates', help='Display dates.', is_flag=True) @click.option('--fqdns', help='Display fqdns.', is_flag=True) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, id, vhosts, dates, fqdns, limit): """ List hosted certificates. """ justify = 10 options = {'items_per_page': limit, 'state': 'created'} output_keys = [] if id: output_keys.append('id') output_keys.append('subject') if dates: output_keys.extend(['date_created', 'date_expire']) justify = 12 if fqdns: output_keys.append('fqdns') if vhosts: output_keys.append('vhosts') result = gandi.hostedcert.list(options) for num, hcert in enumerate(result): if num: gandi.separator_line() if fqdns or vhosts: hcert = gandi.hostedcert.info(hcert['id']) output_hostedcert(gandi, hcert, output_keys, justify) return result @certstore.command() @click.argument('resource', nargs=-1, required=True) @pass_gandi def info(gandi, resource): """ Display information about a hosted certificate. Resource can be a FQDN or an ID """ output_keys = ['id', 'subject', 'date_created', 'date_expire', 'fqdns', 'vhosts'] result = gandi.hostedcert.infos(resource) for num, hcert in enumerate(result): if num: gandi.separator_line() output_hostedcert(gandi, hcert, output_keys) return result @certstore.command() @click.option('--pk', '--private-key', required=True, help='Private key used to generate this CRT.') @click.option('--crt', '--certificate', required=False, help='The certificate.') @click.option('--crt-id', '--certificate-id', type=click.INT, required=False, help='The certificate.') @pass_gandi def create(gandi, private_key, certificate, certificate_id): """ Create a new hosted certificate. """ if not certificate and not certificate_id: gandi.echo('One of --certificate or --certificate-id is needed.') return if certificate and certificate_id: gandi.echo('Only one of --certificate or --certificate-id is needed.') if os.path.isfile(private_key): with open(private_key) as fhandle: private_key = fhandle.read() if certificate: if os.path.isfile(certificate): with open(certificate) as fhandle: certificate = fhandle.read() else: cert = gandi.certificate.info(certificate_id) certificate = gandi.certificate.pretty_format_cert(cert) result = gandi.hostedcert.create(private_key, certificate) output_keys = ['id', 'subject', 'date_created', 'date_expire', 'fqdns', 'vhosts'] output_hostedcert(gandi, result, output_keys) return result @certstore.command() @click.argument('resource', nargs=-1, required=True) @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def delete(gandi, resource, force): """ Delete a hosted certificate. Resource can be a FQDN or an ID """ infos = gandi.hostedcert.infos(resource) if not infos: return if not force: proceed = click.confirm('Are you sure to delete the following hosted ' 'certificates ?\n' + '\n'.join(['%s: %s' % (res['id'], res['subject']) for res in infos]) + '\n') if not proceed: return for res in infos: gandi.hostedcert.delete(res['id']) gandi.cli-1.2/gandi/cli/commands/paas.py0000644000175000017500000003047013227414171021020 0ustar sayounsayoun00000000000000""" PaaS instances namespace commands. """ import os import click from click.exceptions import UsageError from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_paas, output_generic, randomstring, DatacenterLimited ) from gandi.cli.core.params import ( pass_gandi, DATACENTER, SNAPSHOTPROFILE_PAAS, PAAS_TYPE, option, ) @cli.group(name='paas') @pass_gandi def paas(gandi): """Commands related to Simple Hosting.""" @paas.command() @click.option('--state', default=None, help='Filter results by state.') @click.option('--id', help='Display ids.', is_flag=True) @click.option('--vhosts', help='Display vhosts.', default=True, is_flag=True) @click.option('--type', help='Display types.', is_flag=True) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, state, id, vhosts, type, limit): """List PaaS instances.""" options = { 'items_per_page': limit, } if state: options['state'] = state output_keys = ['name', 'state'] if id: output_keys.append('id') if vhosts: output_keys.append('vhost') if type: output_keys.append('type') paas_hosts = {} result = gandi.paas.list(options) for num, paas in enumerate(result): paas_hosts[paas['id']] = [] if vhosts: list_vhost = gandi.vhost.list({'paas_id': paas['id']}) for host in list_vhost: paas_hosts[paas['id']].append(host['name']) if num: gandi.separator_line() output_paas(gandi, paas, [], paas_hosts[paas['id']], output_keys) return result @paas.command() @click.argument('resource') @click.option('--stat', default=False, is_flag=True, help='Display cached page statistic based on the last 24 hours.') @pass_gandi def info(gandi, resource, stat): """Display information about a PaaS instance. Resource can be a vhost, a hostname, or an ID Cache statistics are based on 24 hours data. """ output_keys = ['name', 'type', 'size', 'memory', 'console', 'vhost', 'dc', 'sftp_server', 'git_server', 'snapshot'] paas = gandi.paas.info(resource) paas_hosts = [] list_vhost = gandi.vhost.list({'paas_id': paas['id']}) df = gandi.paas.quota(paas['id']) paas.update({'df': df}) if stat: cache = gandi.paas.cache(paas['id']) paas.update({'cache': cache}) for host in list_vhost: paas_hosts.append(host['name']) output_paas(gandi, paas, [], paas_hosts, output_keys) return paas @paas.command() @click.option('--origin', default='gandi', help="Set the origin remote's name") @click.option('--directory', help='Specify the destination directory') @click.option('--vhost', required=False, default='default') @click.argument('name', required=True) @pass_gandi def clone(gandi, name, vhost, directory, origin): """Clone a remote vhost in a local git repository.""" if vhost != 'default': directory = vhost else: directory = name if not directory else directory return gandi.paas.clone(name, vhost, directory, origin) @paas.command() @click.option('--vhost', default='default', help="Add a remote for a given instance's vhost to the local " "git repository") @click.option('--remote', default='gandi', help="Specify the remote's name") @click.argument('name', required=True) @pass_gandi def attach(gandi, name, vhost, remote): """Add remote for an instance's default vhost to the local git repository. """ return gandi.paas.attach(name, vhost, remote) @cli.command() @click.option('--remote', default='gandi', help="Specify the remote's name") @click.option('--branch', default='master', help="Specify the branch to deploy") @pass_gandi def deploy(gandi, remote, branch): """Deploy code on the instance's remote vhost corresponding to current directory. """ return gandi.paas.deploy(remote, branch) @paas.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('resource', nargs=-1, required=True) @pass_gandi def delete(gandi, background, force, resource): """Delete a PaaS instance. Resource can be a vhost, a hostname, or an ID """ output_keys = ['id', 'type', 'step'] possible_resources = gandi.paas.resource_list() for item in resource: if item not in possible_resources: gandi.echo('Sorry PaaS instance %s does not exist' % item) gandi.echo('Please use one of the following: %s' % possible_resources) return if not force: instance_info = "'%s'" % ', '.join(resource) proceed = click.confirm("Are you sure to delete PaaS instance %s?" % instance_info) if not proceed: return opers = gandi.paas.delete(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @paas.command() @click.option('--name', default=None, help='Name of the PaaS instance, will be generated if not ' 'provided.') @option('--size', default='s', type=click.Choice(['s', 's+', 'm', 'l', 'xl', 'xxl']), help='Size of the PaaS instance.') @option('--type', default='pythonpgsql', type=PAAS_TYPE, help='Type of the PaaS instance.') @option('--quantity', default=0, help='Additional disk amount (in GB).') @option('--duration', default='1m', help='Number of month, suffixed with m.') @option('--datacenter', type=DATACENTER, default='LU-BI1', help='Datacenter where the PaaS will be spawned.') @click.option('--vhosts', default=None, help='Virtual host(s) to be linked to the instance.') @click.option('--ssl', help='Get ssl on that vhost.', is_flag=True) @click.option('--pk', '--private-key', help='Private key used to generate the ssl Certificate.') @click.option('--poll-cert', help='Will wait for the certificate creation.', is_flag=True) @click.option('--password', help='Use command-line supplied password.') @click.option('--snapshotprofile', default=None, type=SNAPSHOTPROFILE_PAAS, help='Set a snapshot profile associated to this paas disk.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @option('--sshkey', multiple=True, help='Authorize ssh authentication for the given ssh key.') @pass_gandi def create(gandi, name, size, type, quantity, duration, datacenter, vhosts, password, snapshotprofile, background, sshkey, ssl, private_key, poll_cert): """Create a new PaaS instance and initialize associated git repository. you can specify a configuration entry named 'sshkey' containing path to your sshkey file $ gandi config set [-g] sshkey ~/.ssh/id_rsa.pub or getting the sshkey "my_key" from your gandi ssh keyring $ gandi config set [-g] sshkey my_key to know which PaaS instance type to use as type $ gandi paas types """ try: gandi.datacenter.is_opened(datacenter, 'paas') except DatacenterLimited as exc: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) if not password: password = click.prompt('password', hide_input=True, confirmation_prompt=True) if not name: name = randomstring('paas') if vhosts and not gandi.hostedcert.activate_ssl(vhosts, ssl, private_key, poll_cert): return result = gandi.paas.create(name, size, type, quantity, duration, datacenter, vhosts, password, snapshotprofile, background, sshkey) return result @paas.command() @click.option('--name', type=click.STRING, default=None, help='Name of the PaaS instance.') @click.option('--size', default=None, type=click.Choice(['s', 's+', 'm', 'x', 'xl', 'xxl']), help='Size of the PaaS instance.') @click.option('--quantity', type=click.INT, default=0, help='Additional disk amount (in GB).') @click.option('--password', default=False, is_flag=True, help='Password of the PaaS instance.') @click.option('--sshkey', multiple=True, help='Authorize ssh authentication for the given ssh key.') @click.option('--upgrade', default=False, is_flag=True, help='Upgrade the instance to the last system image if needed.') @click.option('--console', default=None, help='Activate or deactivate the Console.') @click.option('--snapshotprofile', default=None, type=click.INT, help='Set a snapshot profile associated to this paas disk.') @click.option('--reset-mysql-password', default=None, help='Reset mysql password for root.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--delete-snapshotprofile', default=False, is_flag=True, help='Remove a snapshot profile associated to this paas disk.') @pass_gandi @click.argument('resource') def update(gandi, resource, name, size, quantity, password, sshkey, upgrade, console, snapshotprofile, reset_mysql_password, background, delete_snapshotprofile): """Update a PaaS instance. Resource can be a Hostname or an ID """ if snapshotprofile and delete_snapshotprofile: raise UsageError('You must not set snapshotprofile and ' 'delete-snapshotprofile.') pwd = None if password: pwd = click.prompt('password', hide_input=True, confirmation_prompt=True) if delete_snapshotprofile: snapshotprofile = '' result = gandi.paas.update(resource, name, size, quantity, pwd, sshkey, upgrade, console, snapshotprofile, reset_mysql_password, background) if background: gandi.pretty_echo(result) return result @paas.command() @click.argument('resource', nargs=-1, required=True) @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False)') @pass_gandi def restart(gandi, resource, background, force): """Restart a PaaS instance. Resource can be a vhost, a hostname, or an ID """ output_keys = ['id', 'type', 'step'] possible_resources = gandi.paas.resource_list() for item in resource: if item not in possible_resources: gandi.echo('Sorry PaaS instance %s does not exist' % item) gandi.echo('Please use one of the following: %s' % possible_resources) return if not force: instance_info = "'%s'" % ', '.join(resource) proceed = click.confirm("Are you sure to restart PaaS instance %s?" % instance_info) if not proceed: return opers = gandi.paas.restart(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers @paas.command() @pass_gandi def types(gandi): """List types PaaS instances.""" options = {} types = gandi.paas.type_list(options) for type_ in types: gandi.echo(type_['name']) return types @paas.command() @click.argument('resource') @pass_gandi def console(gandi, resource): """Open a console on a PaaS. Resource can be a hostname or an ID """ gandi.echo('/!\ Use ~. ssh escape key to exit.') gandi.paas.console(resource) gandi.cli-1.2/gandi/cli/commands/sshkey.py0000644000175000017500000000465713164644514021420 0ustar sayounsayoun00000000000000""" SSH keys namespace commands. """ import click from click.exceptions import UsageError from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_sshkey from gandi.cli.core.params import pass_gandi @cli.group(name='sshkey') @pass_gandi def sshkey(gandi): """Commands related to hosting sshkeys.""" @sshkey.command() @click.option('--id', help='Display ids.', is_flag=True) @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @pass_gandi def list(gandi, id, limit): """ List SSH keys. """ options = { 'items_per_page': limit, } output_keys = ['name', 'fingerprint'] if id: output_keys.append('id') result = gandi.sshkey.list(options) for num, sshkey in enumerate(result): if num: gandi.separator_line() output_sshkey(gandi, sshkey, output_keys) return result @sshkey.command() @click.argument('resource', nargs=-1, required=True) @click.option('--id', help='Display ids.', is_flag=True) @click.option('--value', help='Display value.', is_flag=True) @pass_gandi def info(gandi, resource, id, value): """Display information about an SSH key. Resource can be a name or an ID """ output_keys = ['name', 'fingerprint'] if id: output_keys.append('id') if value: output_keys.append('value') ret = [] for item in resource: sshkey = gandi.sshkey.info(item) ret.append(output_sshkey(gandi, sshkey, output_keys)) return ret @sshkey.command() @click.option('--name', help='SSH key name.', required=True) @click.option('--value', help='Content of the SSH key.') @click.option('--filename', type=click.File('r'), help='SSH key file.') @pass_gandi def create(gandi, name, value=None, filename=None): """ Create a new SSH key. """ if not value and not filename: raise UsageError('You must set value OR filename.') if value and filename: raise UsageError('You must not set value AND filename.') if filename: value = filename.read() ret = gandi.sshkey.create(name, value) output_keys = ['id', 'name', 'fingerprint'] return output_sshkey(gandi, ret, output_keys) @sshkey.command() @click.argument('resource', nargs=-1, required=True) @pass_gandi def delete(gandi, resource): """Delete SSH keys. Resource can be a name or an ID """ for item in resource: gandi.sshkey.delete(item) gandi.cli-1.2/gandi/cli/commands/vhost.py0000644000175000017500000001127313164644514021245 0ustar sayounsayoun00000000000000""" Virtual hosts namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_generic, output_vhost from gandi.cli.core.params import pass_gandi @cli.group(name='vhost') @pass_gandi def vhost(gandi): """Commands related to Simple Hosting virtual hosts.""" @vhost.command() @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @click.option('--id', help='Display ids.', is_flag=True) @click.option('--names', help='Display names.', is_flag=True) @pass_gandi def list(gandi, limit, id, names): """ List vhosts. """ options = { 'items_per_page': limit, } output_keys = ['name', 'state', 'date_creation'] if id: # When we will have more than paas vhost, we will append rproxy_id output_keys.append('paas_id') paas_names = {} if names: output_keys.append('paas_name') paas_names = gandi.paas.list_names() result = gandi.vhost.list(options) for num, vhost in enumerate(result): paas = paas_names.get(vhost['paas_id']) if num: gandi.separator_line() output_vhost(gandi, vhost, paas, output_keys) return result @vhost.command() @click.option('--id', help='Display ids.', is_flag=True) @click.argument('resource', nargs=-1, required=True) @pass_gandi def info(gandi, resource, id): """ Display information about a vhost. Resource must be the vhost fqdn. """ output_keys = ['name', 'state', 'date_creation', 'paas_name', 'ssl'] if id: # When we will have more than paas vhost, we will append rproxy_id output_keys.append('paas_id') paas_names = gandi.paas.list_names() ret = [] paas = None for num, item in enumerate(resource): vhost = gandi.vhost.info(item) try: hostedcert = gandi.hostedcert.infos(vhost['name']) vhost['ssl'] = 'activated' if hostedcert else 'disabled' except ValueError: vhost['ssl'] = 'disabled' paas = paas_names.get(vhost['paas_id']) if num: gandi.separator_line() ret.append(output_vhost(gandi, vhost, paas, output_keys)) return ret @vhost.command() @click.option('--paas', required=True, help='PaaS instance on which to create it.') @click.option('--ssl', help='Get ssl on that vhost.', is_flag=True) @click.option('--pk', '--private-key', help='Private key used to generate the ssl Certificate.') @click.option('--alter-zone', help='Will update the domain zone.', is_flag=True) @click.option('--poll-cert', help='Will wait for the certificate creation.', is_flag=True) @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('vhost', required=True) @pass_gandi def create(gandi, vhost, paas, ssl, private_key, alter_zone, poll_cert, background): """ Create a new vhost. """ if not gandi.hostedcert.activate_ssl(vhost, ssl, private_key, poll_cert): return paas_info = gandi.paas.info(paas) result = gandi.vhost.create(paas_info, vhost, alter_zone, background) if background: gandi.pretty_echo(result) return result @vhost.command() @click.option('--ssl', help='Get ssl on that vhost.', is_flag=True) @click.option('--pk', '--private-key', help='Private key used to generate the ssl Certificate.') @click.option('--poll-cert', help='Will wait for the certificate creation.', is_flag=True) @click.argument('resource', nargs=1, required=True) @pass_gandi def update(gandi, resource, ssl, private_key, poll_cert): """ Update a vhost. Right now you can only activate ssl on the vhost. """ gandi.hostedcert.activate_ssl(resource, ssl, private_key, poll_cert) @vhost.command() @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.argument('resource', required=True) @pass_gandi def delete(gandi, resource, force, background): """ Delete a vhost. """ output_keys = ['name', 'paas_id', 'state', 'date_creation'] if not force: proceed = click.confirm('Are you sure to delete vhost %s?' % resource) if not proceed: return opers = gandi.vhost.delete(resource, background) if background: for oper in opers: output_generic(gandi, oper, output_keys) return opers gandi.cli-1.2/gandi/cli/commands/ip.py0000644000175000017500000002113013211520331020462 0ustar sayounsayoun00000000000000""" Ip namespace commands. """ import click from click.exceptions import UsageError from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_ip, DatacenterLimited ) from gandi.cli.core.params import (pass_gandi, DATACENTER, IP_TYPE, option, IntChoice) @cli.group(name='ip') @pass_gandi def ip(gandi): """Commands related to hosting IPs.""" @ip.command() @click.option('--datacenter', type=DATACENTER, default=None, help='Filter by datacenter.') @click.option('--type', default=None, type=IP_TYPE, help='Filter by type.') @click.option('--id', help='Display ids.', is_flag=True) @click.option('--attached', help='Only display attached ip.', is_flag=True) @click.option('--detached', help='Only display detached ip.', is_flag=True) @click.option('--version', help='Display ip version.', is_flag=True) @click.option('--reverse', help='Display ip reverse.', is_flag=True) @click.option('--vm', help='Display ip vm.', is_flag=True) @click.option('--vlan', default=None, help='Filter by vlan.') @pass_gandi def list(gandi, datacenter, type, id, attached, detached, version, reverse, vm, vlan): """List ips.""" if attached and detached: gandi.echo("You can't set --attached and --detached at the same time.") return output_keys = ['ip', 'state', 'dc', 'type'] if id: output_keys.append('id') if version: output_keys.append('version') if vm: output_keys.append('vm') if reverse: output_keys.append('reverse') options = {} opt_dc = {} if datacenter: datacenter_id = int(gandi.datacenter.usable_id(datacenter)) options['datacenter_id'] = datacenter_id opt_dc = {'datacenter_id': datacenter_id} iface_options = {} if type: iface_options['type'] = type if vlan: iface_options['vlan'] = vlan if attached: iface_options['state'] = 'used' elif detached: iface_options['state'] = 'free' if iface_options: ifaces = gandi.iface.list(iface_options) options['iface_id'] = [iface['id'] for iface in ifaces] iface_options.update(opt_dc) datacenters = gandi.datacenter.list() ips = gandi.ip.list(options) ifaces = dict([(iface['id'], iface) for iface in gandi.iface.list(iface_options)]) vms = dict([(vm_['id'], vm_) for vm_ in gandi.iaas.list(opt_dc)]) for num, ip_ in enumerate(ips): if num: gandi.separator_line() output_ip(gandi, ip_, datacenters, vms, ifaces, output_keys) return ips @ip.command() @click.argument('resource') @pass_gandi def info(gandi, resource): """Display information about an ip. Resource can be an ip or id. """ output_keys = ['ip', 'state', 'dc', 'type', 'vm', 'reverse'] datacenters = gandi.datacenter.list() ip = gandi.ip.info(resource) iface = gandi.iface.info(ip['iface_id']) vms = None if iface.get('vm_id'): vm = gandi.iaas.info(iface['vm_id']) vms = {vm['id']: vm} output_ip(gandi, ip, datacenters, vms, {iface['id']: iface}, output_keys) return ip @ip.command() @click.argument('ip') @click.option('--reverse', help='Update reverse (PTR record) for this IP') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @pass_gandi def update(gandi, ip, reverse, background): """Update an ip.""" if not reverse: return return gandi.ip.update(ip, {'reverse': reverse}, background) @ip.command() @click.argument('ip') @click.argument('vm') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def attach(gandi, ip, vm, background, force): """Attach an ip to a vm. ip can be an ip id or ip vm can be a vm id or name. """ try: ip_ = gandi.ip.info(ip) vm_ = gandi.iaas.info(vm) except UsageError: gandi.error("Can't find this ip %s" % ip) iface = gandi.iface.info(ip_['iface_id']) if iface.get('vm_id'): if vm_ and iface['vm_id'] == vm_.get('id'): gandi.echo('This ip is already attached to this vm.') return if not force: proceed = click.confirm('Are you sure you want to detach' ' %s from vm %s' % (ip_['ip'], iface['vm_id'])) if not proceed: return return gandi.ip.attach(ip, vm, background, force) @ip.command() @click.option('--datacenter', type=DATACENTER, help='Datacenter where the ip will be created.') @option('--bandwidth', type=click.INT, default=102400, help="Network bandwidth in kbit/s used to create the VM's first " "network interface.") @option('--ip-version', type=IntChoice(['4', '6']), default=4, help='Version of created IP.') @click.option('--vlan', help='The vlan to which attach this ip if any.') @click.option('--ip', help='The ip if you try to create a private ip.') @click.option('--attach', help='The vm you want to attach if any.') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @pass_gandi def create(gandi, datacenter, bandwidth, ip_version, vlan, ip, attach, background): """Create a public or private ip """ if ip_version != 4 and vlan: gandi.echo('You must have an --ip-version to 4 when having a vlan.') return if ip and not vlan: gandi.echo('You must have a --vlan when giving an --ip.') return vm_ = gandi.iaas.info(attach) if attach else None if datacenter and vm_: dc_id = gandi.datacenter.usable_id(datacenter) if dc_id != vm_['datacenter_id']: gandi.echo('The datacenter you provided does not match the ' 'datacenter of the vm you want to attach to.') return if not datacenter: datacenter = vm_['datacenter_id'] if vm_ else 'LU' try: gandi.datacenter.is_opened(datacenter, 'iaas') except DatacenterLimited as exc: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) return gandi.ip.create(ip_version, datacenter, bandwidth, attach, vlan, ip, background) @ip.command() @click.argument('resource') @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def detach(gandi, resource, background, force): """Detach an ip from it's currently attached vm. resource can be an ip id or ip. """ if not force: proceed = click.confirm('Are you sure you want to detach ip %s?' % resource) if not proceed: return return gandi.ip.detach(resource, background, force) @ip.command() @click.argument('resource', nargs=-1, required=True) @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @pass_gandi def delete(gandi, resource, background, force): """Delete one or more IPs (after detaching them from VMs if necessary). resource can be an ip id or ip. """ resource = sorted(tuple(set(resource))) possible_resources = gandi.ip.resource_list() # check that each IP can be deleted for item in resource: if item not in possible_resources: gandi.echo('Sorry interface %s does not exist' % item) gandi.echo('Please use one of the following: %s' % possible_resources) return if not force: proceed = click.confirm('Are you sure you want to delete ip(s) %s' % ', '.join(resource)) if not proceed: return return gandi.ip.delete(resource, background, force) gandi.cli-1.2/gandi/cli/commands/config.py0000644000175000017500000000455713164644514021356 0ustar sayounsayoun00000000000000""" Configuration namespace commands. """ import sys import os import subprocess import click from click.exceptions import Abort from gandi.cli.core.cli import cli from gandi.cli.core.params import pass_gandi @cli.group(name='config') @pass_gandi def config(gandi): """Commands related to Gandi CLI configuration.""" @config.command() @click.option('-g', help='Get from global configuration only ' '(default=env -> local -> global).', is_flag=True, default=False) @click.argument('key') @pass_gandi def get(gandi, g, key): """Display value of a given config key.""" val = gandi.get(key=key, global_=g) if not val: gandi.echo("No value found.") sys.exit(1) gandi.echo(val) @config.command() @click.option('-g', help='Edit global configuration (default=local).', is_flag=True, default=False) @click.argument('key') @click.argument('value') @pass_gandi def set(gandi, g, key, value): """Update or create config key/value.""" gandi.configure(global_=g, key=key, val=value) @config.command() @click.option('-g', help='Edit global configuration (default=local).', is_flag=True, default=False) @pass_gandi def edit(gandi, g): """Edit config file with prefered text editor""" config_file = gandi.home_config if g else gandi.local_config path = os.path.expanduser(config_file) editor = gandi.get('editor') if not editor: try: editor = click.prompt("Please enter the path of your prefered " "editor. eg: '/usr/bin/vi' or 'vi'") except Abort: gandi.echo(""" Warning: editor is not configured. You can use both 'gandi config set [-g] editor ' or the $EDITOR environment variable to configure it.""") sys.exit(1) subprocess.call([editor, path]) @config.command() @click.option('-g', help='Delete on global configuration (default=local).', is_flag=True, default=False) @click.argument('key') @pass_gandi def delete(gandi, g, key): """Delete a key/value pair from configuration""" gandi.delete(global_=g, key=key) @config.command() @click.option('-g', help='Display global configuration (default=local).', is_flag=True, default=False) @pass_gandi def list(gandi, g): """Display config file content""" gandi.pretty_echo(gandi.list(global_=g)) gandi.cli-1.2/gandi/cli/commands/mail.py0000644000175000017500000001221113164644514021015 0ustar sayounsayoun00000000000000""" Mail namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_mailbox, output_list from gandi.cli.core.params import pass_gandi, EMAIL_TYPE @cli.group(name='mail') @pass_gandi def mail(gandi): """Commands related to domain mailboxes.""" @mail.command() @click.option('--limit', help='Limit number of results.', default=100, show_default=True) @click.argument('domain', metavar='domain.tld') @pass_gandi def list(gandi, domain, limit): """List mailboxes created on a domain.""" options = {'items_per_page': limit} mailboxes = gandi.mail.list(domain, options) output_list(gandi, [mbox['login'] for mbox in mailboxes]) return mailboxes @mail.command() @click.argument('email', type=EMAIL_TYPE, metavar='login@domain.tld') @pass_gandi def info(gandi, email): """Display information about a mailbox.""" login, domain = email output_keys = ['login', 'aliases', 'fallback', 'quota', 'responder'] mailbox = gandi.mail.info(domain, login) output_mailbox(gandi, mailbox, output_keys) return mailbox @mail.command() @click.option('--quota', '-q', help='Set quota on mailbox. 0 is unlimited.', default=None, type=click.INT) @click.option('--fallback', '-f', help='Add fallback address.', default=None) @click.option('--alias', '-a', help='Add mailbox alias.', multiple=True, required=False) @click.option('--password', '-p', default=None, type=click.STRING, required=False, help='Prompt a password to create a mailbox.') @click.argument('email', type=EMAIL_TYPE, metavar='login@domain.tld') @pass_gandi def create(gandi, email, quota, fallback, alias, password): """Create a mailbox.""" login, domain = email options = {} if not password: password = click.prompt('password', hide_input=True, confirmation_prompt=True) options['password'] = password if quota is not None: options['quota'] = quota if fallback is not None: options['fallback_email'] = fallback result = gandi.mail.create(domain, login, options, alias) return result @mail.command() @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.argument('email', type=EMAIL_TYPE, metavar='login@domain.tld') @pass_gandi def delete(gandi, email, force): """Delete a mailbox.""" login, domain = email if not force: proceed = click.confirm('Are you sure to delete the ' 'mailbox %s@%s ?' % (login, domain)) if not proceed: return result = gandi.mail.delete(domain, login) return result @mail.command() @click.option('--password', '-p', help='Prompt a password to set a mailbox.', is_flag=True) @click.option('--quota', '-q', help='Set quota on mailbox. 0 is unlimited.', default=None, type=click.INT) @click.option('--fallback', '-f', help='Add fallback address.', default=None, show_default=True) @click.option('--alias-add', '-a', help='Add mailbox alias.', multiple=True, required=False) @click.option('--alias-del', '-d', help='Delete mailbox alias.', multiple=True, required=False) @click.argument('email', type=EMAIL_TYPE, metavar='login@domain.tld') @pass_gandi def update(gandi, email, password, quota, fallback, alias_add, alias_del): """Update a mailbox.""" options = {} if password: password = click.prompt('password', hide_input=True, confirmation_prompt=True) options['password'] = password if quota is not None: options['quota'] = quota if fallback is not None: options['fallback_email'] = fallback login, domain = email result = gandi.mail.update(domain, login, options, alias_add, alias_del) return result @mail.command() @click.option('--bg', '--background', default=False, is_flag=True, help='Run command in background mode (default=False).') @click.option('--force', '-f', is_flag=True, help='This is a dangerous option that will cause CLI to continue' ' without prompting. (default=False).') @click.option('--alias', '-a', help='Purge all aliases.', default=False, is_flag=True) @click.argument('email', type=EMAIL_TYPE, metavar='login@domain.tld') @pass_gandi def purge(gandi, email, background, force, alias): """Purge a mailbox.""" login, domain = email if alias: if not force: proceed = click.confirm('Are you sure to purge all aliases for ' 'mailbox %s@%s ?' % (login, domain)) if not proceed: return result = gandi.mail.set_alias(domain, login, []) else: if not force: proceed = click.confirm('Are you sure to purge mailbox %s@%s ?' % (login, domain)) if not proceed: return result = gandi.mail.purge(domain, login, background) return result gandi.cli-1.2/gandi/cli/commands/webacc.py0000644000175000017500000004006013207550753021321 0ustar sayounsayoun00000000000000""" Webaccelerator namespace commands. """ import click from gandi.cli.core.cli import cli from gandi.cli.core.utils import ( output_generic, output_json, output_sub_generic, DatacenterLimited ) from gandi.cli.core.params import ( pass_gandi, BACKEND, DATACENTER, WEBACC_NAME, WEBACC_VHOST_NAME ) @cli.group(name='webacc') @pass_gandi def webacc(gandi): """Commands related to hosting web accelerators.""" @webacc.command() @click.option('--limit', help="Limit the number of results", default=100, show_default=True) @click.option('--format', type=click.Choice(['json', 'pretty-json']), required=False, help="Choose the output format") @pass_gandi def list(gandi, limit, format): """ List webaccelerators """ options = { 'items_per_page': limit, } result = gandi.webacc.list(options) if format: output_json(gandi, format, result) return result output_keys = ['name', 'state', 'ssl'] for num, webacc in enumerate(result): if num: gandi.separator_line('-', 4) webacc['ssl'] = 'Enabled' if webacc['ssl_enable'] else 'Disable' output_generic(gandi, webacc, output_keys, justify=14) gandi.echo('Vhosts :') for num, vhost in enumerate(webacc['vhosts']): output_vhosts = ['vhost', 'ssl'] vhost['vhost'] = vhost['name'] vhost['ssl'] = 'Disable' if vhost['cert_id'] is None else 'Enabled' output_sub_generic(gandi, vhost, output_vhosts, justify=14) gandi.echo('') gandi.echo('Backends :') for server in webacc['servers']: try: ip = gandi.ip.info(server['ip']) iface = gandi.iface.info(ip['iface_id']) vm_info = gandi.iaas.info(iface['vm_id']) server['name'] = vm_info['hostname'] output_servers = ['name', 'ip', 'port', 'state'] except Exception: warningmsg = ('\tBackend with ip address %s no longer ' 'exists.\n\tYou should remove it.' % server['ip']) gandi.echo(warningmsg) output_servers = ['ip', 'port', 'state'] output_sub_generic(gandi, server, output_servers, justify=14) gandi.echo('') return result @webacc.command() @click.option('--format', type=click.Choice(['json', 'pretty-json']), required=False, help="Choose the output format") @click.argument('resource', type=WEBACC_NAME) @pass_gandi def info(gandi, resource, format): """ Display information about a webaccelerator """ result = gandi.webacc.info(resource) if format: output_json(gandi, format, result) return result output_base = { 'name': result['name'], 'algorithm': result['lb']['algorithm'], 'datacenter': result['datacenter']['name'], 'state': result['state'], 'ssl': 'Disable' if result['ssl_enable'] is False else 'Enabled' } output_keys = ['name', 'state', 'datacenter', 'ssl', 'algorithm'] output_generic(gandi, output_base, output_keys, justify=14) gandi.echo('Vhosts :') for vhost in result['vhosts']: output_vhosts = ['vhost', 'ssl'] vhost['vhost'] = vhost['name'] vhost['ssl'] = 'None' if vhost['cert_id'] is None else 'Exists' output_sub_generic(gandi, vhost, output_vhosts, justify=14) gandi.echo('') gandi.echo('Backends :') for server in result['servers']: try: ip = gandi.ip.info(server['ip']) iface = gandi.iface.info(ip['iface_id']) server['name'] = gandi.iaas.info(iface['vm_id'])['hostname'] output_servers = ['name', 'ip', 'port', 'state'] except: warningmsg = ('\tBackend with ip address %s no longer exists.' '\n\tYou should remove it.' % server['ip']) gandi.echo(warningmsg) output_servers = ['ip', 'port', 'state'] output_sub_generic(gandi, server, output_servers, justify=14) gandi.echo('') gandi.echo('Probe :') output_probe = ['state', 'host', 'interval', 'method', 'response', 'threshold', 'timeout', 'url', 'window'] result['probe']['state'] = ('Disable' if result['probe']['enable'] is False else 'Enabled') output_sub_generic(gandi, result['probe'], output_probe, justify=14) return result @webacc.command() @click.option('--datacenter', '-dc', type=DATACENTER, help="Datacenter where the webaccelerator will be created", required=True) @click.option('--backend', '-b', type=BACKEND, multiple=True, help="Backend to add in the webaccelerator, use ip:port") @click.option('--port', '-p', type=click.INT, required=False, help="set a default port backend if not specified with backend") @click.option('--vhost', '-v', help="Vhost to add in the webaccelerator", multiple=True) @click.option('--ssl', help='Get ssl on that vhost.', is_flag=True) @click.option('--pk', '--private-key', help='Private key used to generate the ssl Certificate.') @click.option('--poll-cert', help='Will wait for the certificate creation.', is_flag=True) @click.option('--algorithm', type=click.Choice(['client-ip', 'round-robin']), help="Choose the loadbalancer algorithm", default='client-ip') @click.option('--ssl-enable', is_flag=True, help="Activate SSL support on the webaccelerator") @click.option('--zone-alter', is_flag=True, help="Alter the zone file of the domain for the vhost if domains" "are registred at Gandi") @click.argument('name') @pass_gandi def create(gandi, name, datacenter, backend, port, vhost, algorithm, ssl_enable, zone_alter, ssl, private_key, poll_cert): """ Create a webaccelerator """ try: gandi.datacenter.is_opened(datacenter, 'iaas') except DatacenterLimited as exc: gandi.echo('/!\ Datacenter %s will be closed on %s, ' 'please consider using another datacenter.' % (datacenter, exc.date)) backends = backend for backend in backends: # Check if a port is set for each backend, else set a default port if 'port' not in backend: if not port: backend['port'] = click.prompt('Please set a port for ' 'backends. If you want to set ' 'different port for each ' 'backend, use `-b ip:port`', type=int) else: backend['port'] = port if vhost and not gandi.hostedcert.activate_ssl(vhost, ssl, private_key, poll_cert): return result = gandi.webacc.create(name, datacenter, backends, vhost, algorithm, ssl_enable, zone_alter) return result @webacc.command() @click.option('--name', '-n', help="The name of the webaccelerator") @click.option('--algorithm', type=click.Choice(['client-ip', 'round-robin']), help="Choose the loadbalancer algorithm") @click.option('--ssl-enable', is_flag=True, help="Activate SSL support on the webaccelerator") @click.option('--ssl-disable', is_flag=True, help="Deactivate SSL support on the webaccelerator") @click.argument('resource', required=True, type=WEBACC_NAME) @pass_gandi def update(gandi, resource, name, algorithm, ssl_enable, ssl_disable): """Update a webaccelerator""" result = gandi.webacc.update(resource, name, algorithm, ssl_enable, ssl_disable) return result @webacc.command() @click.option('--vhost', '-v', help="Remove vhosts in the webaccelerator", multiple=True, type=WEBACC_VHOST_NAME) @click.option('--backend', '-b', help="Remove backends in the webaccelerator", multiple=True, type=BACKEND) @click.option('--port', '-p', type=click.INT, required=False, help="The backend port if not specified with backend") @click.option('--webacc', '-w', type=WEBACC_NAME, required=False, help='The webaccelerator name') @pass_gandi def delete(gandi, webacc, vhost, backend, port): """ Delete a webaccelerator, a vhost or a backend """ result = [] if webacc: result = gandi.webacc.delete(webacc) if backend: backends = backend for backend in backends: if 'port' not in backend: if not port: backend['port'] = click.prompt('Please set a port for ' 'backends. If you want to ' ' different port for ' 'each backend, use `-b ' 'ip:port`', type=int) else: backend['port'] = port result = gandi.webacc.backend_remove(backend) if vhost: vhosts = vhost for vhost in vhosts: result = gandi.webacc.vhost_remove(vhost) return result @webacc.command() @click.option('--vhost', '-v', help="Add vhosts in the webaccelerator", multiple=True) @click.option('--ssl', help='Get ssl on that vhost.', is_flag=True) @click.option('--pk', '--private-key', help='Private key used to generate the ssl Certificate.') @click.option('--poll-cert', help='Will wait for the certificate creation.', is_flag=True) @click.option('--zone-alter', is_flag=True, help="Alter and activate zone file if Gandi DNS are used for" " the domain",) @click.option('--backend', '-b', help="Add backends in the webaccelerator", type=BACKEND, multiple=True) @click.option('--port', '-p', type=click.INT, required=False, help="set a default port backend if not specified with backend") @click.argument('resource', type=WEBACC_NAME) @pass_gandi def add(gandi, resource, vhost, zone_alter, backend, port, ssl, private_key, poll_cert): """ Add a backend or a vhost on a webaccelerator """ result = [] if backend: backends = backend for backend in backends: # Check if a port is set for each backend, else set a default port if 'port' not in backend: if not port: backend['port'] = click.prompt('Please set a port for ' 'backends. If you want to ' ' different port for ' 'each backend, use `-b ' 'ip:port`', type=int) else: backend['port'] = port result = gandi.webacc.backend_add(resource, backend) if vhost: if not gandi.hostedcert.activate_ssl(vhost, ssl, private_key, poll_cert): return vhosts = vhost for vhost in vhosts: params = {'vhost': vhost} if zone_alter: params['zone_alter'] = zone_alter result = gandi.webacc.vhost_add(resource, params) return result @webacc.command() @click.option('--backend', '-b', help="Enable backends in the webaccelerator", multiple=True, type=BACKEND) @click.option('--port', '-p', type=click.INT, required=False, help="set a default port backend if not specified with backend") @click.option('--probe', '-p', help="Enable probe for the webaccelerator", is_flag=True) @click.argument('resource', required=False, type=WEBACC_NAME) @pass_gandi def enable(gandi, resource, backend, port, probe): """ Enable a backend or a probe on a webaccelerator """ result = [] if backend: backends = backend for backend in backends: if 'port' not in backend: if not port: backend['port'] = click.prompt('Please set a port for ' 'backends. If you want to ' ' different port for ' 'each backend, use `-b ' 'ip:port`', type=int) else: backend['port'] = port result = gandi.webacc.backend_enable(backend) if probe: if not resource: gandi.echo('You need to indicate the Webaccelerator name') return result = gandi.webacc.probe_enable(resource) return result @webacc.command() @click.option('--backend', '-b', help="Disable backends in the webaccelerator", multiple=True, type=BACKEND) @click.option('--port', '-p', type=click.INT, required=False, help="set a default port backend if not specified with backend") @click.option('--probe', '-p', help="Disable probe for the webaccelerator", is_flag=True) @click.argument('resource', required=False, type=WEBACC_NAME) @pass_gandi def disable(gandi, resource, backend, port, probe): """ Disable a backend or a probe on a webaccelerator """ result = [] if backend: backends = backend for backend in backends: if 'port' not in backend: if not port: backend['port'] = click.prompt('Please set a port for ' 'backends. If you want to ' ' different port for ' 'each backend, use `-b ' 'ip:port`', type=int) else: backend['port'] = port result = gandi.webacc.backend_disable(backend) if probe: if not resource: gandi.echo('You need to indicate the Webaccelerator name') return result = gandi.webacc.probe_disable(resource) return result @webacc.command() @click.option('--enable', '-e', is_flag=True, help="Enable the probe on the webaccelerator") @click.option('--disable', '-d', is_flag=True, help="Disable the probe on the webaccelerator") @click.option('--test', is_flag=True, help="Test probe on the webaccelerator") @click.option('--host', '-h', help="Set the host to test") @click.option('--interval', '-i', help="Set interval for the probe", type=click.INT) @click.option('--http-method', '-m', help="Choose HTTP method for the probe", type=click.Choice(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])) @click.option('--http-response', '-r', type=click.INT, help="HTTP response code expected by the probe") @click.option('--threshold', '-t', type=click.INT, help="Number of probes to consider in the window") @click.option('--timeout', help="Timeout in seconds", type=click.INT) @click.option('--url', '-u', help="Probe url in the virtual host", type=click.STRING) @click.option('--window', '-w', type=click.INT, help="Total number of probes to consider health decision") @click.argument('resource', type=WEBACC_NAME) @pass_gandi def probe(gandi, resource, enable, disable, test, host, interval, http_method, http_response, threshold, timeout, url, window): """ Manage a probe for a webaccelerator """ result = gandi.webacc.probe(resource, enable, disable, test, host, interval, http_method, http_response, threshold, timeout, url, window) output_keys = ['status', 'timeout'] output_generic(gandi, result, output_keys, justify=14) return result gandi.cli-1.2/gandi/cli/commands/account.py0000644000175000017500000000112713164644514021533 0ustar sayounsayoun00000000000000""" Account namespace commands. """ from gandi.cli.core.cli import cli from gandi.cli.core.utils import output_account from gandi.cli.core.params import pass_gandi @cli.group(name='account') @pass_gandi def account(gandi): """Commands related to accounts.""" @account.command() @pass_gandi def info(gandi): """Display information about hosting account. """ output_keys = ['handle', 'credit', 'prepaid'] account = gandi.account.all() account['prepaid_info'] = gandi.contact.balance().get('prepaid', {}) output_account(gandi, account, output_keys) return account gandi.cli-1.2/gandi/__init__.py0000644000175000017500000000030312441335654017260 0ustar sayounsayoun00000000000000# -*- coding: utf-8 -*- try: import pkg_resources pkg_resources.declare_namespace(__name__) except ImportError: import pkgutil __path__ = pkgutil.extend_path(__path__, __name__) gandi.cli-1.2/setup.cfg0000644000175000017500000000024013227415174015705 0ustar sayounsayoun00000000000000[nosetests] match = ^test nocapture = 1 cover-package = gandi.cli with-coverage = 1 cover-erase = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 gandi.cli-1.2/MANIFEST.in0000644000175000017500000000021512667232423015625 0ustar sayounsayoun00000000000000include README.md include CHANGES.rst include LICENSE recursive-include gandi * recursive-exclude * __pycache__ recursive-exclude * *.py[co] gandi.cli-1.2/setup.py0000644000175000017500000000363413160664756015620 0ustar sayounsayoun00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- import re import os import sys from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.md')).read() CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() with open(os.path.join(here, 'gandi', 'cli', '__init__.py')) as v_file: version = re.compile(r".*__version__ = '(.*?)'", re.S).match(v_file.read()).group(1) requires = ['setuptools', 'pyyaml', 'click>=3.1', 'requests', 'IPy'] tests_require = ['nose', 'coverage', 'tox'] if sys.version_info < (2, 7): tests_require += ['unittest2', 'importlib'] if sys.version_info < (3, 3): tests_require.append('mock') extras_require = { 'test': tests_require, } setup(name='gandi.cli', namespace_packages=['gandi'], version=version, description='Gandi command line interface', long_description=README + '\n\n' + CHANGES, author='Gandi', author_email='feedback@gandi.net', classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Terminals', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', ], url='https://github.com/Gandi/gandi.cli', packages=find_packages(), include_package_data=True, zip_safe=False, install_requires=requires, tests_require=tests_require, test_suite='nose.collector', extras_require=extras_require, entry_points={ 'console_scripts': [ 'gandi = gandi.cli.__main__:main', ], }, ) gandi.cli-1.2/CHANGES.rst0000644000175000017500000004126713227414630015700 0ustar sayounsayoun00000000000000Changelog ========= 1.2 --- * Add support for paas size s+ for creation/update * Fixes #232: Update 'gandi record update' command to allow filtering by name * Fix bug when attempting to migrate a vm which cannot be migrated * Only display DC closed warning if a date is set 1.1 --- * FR-SD5 is now the default datacenter. * Add new 'gandi dns update' command. * Fixes #228: Generate a user password at the creation of a VM * Improve wait for ssh connectivity after 'gandi vm create' command to handle ipv6 * Fix a bug with 'gandi disk migrate' command not working with multiple datacenters choices * Improve documentation for generating username/apikey with Gandi V5 1.0 ---- * New 'dns' namespace to manage DNS records/dnssec through LiveDNS API. * Add new 'gandi vm migrate' command. * Refactor internal click code usage. Remove hackish code to handle nested commands which was limited to only 1 nested level. - This change will break code of users which were using custom commands on top of Gandi CLI, To fix this you have to use the proper click syntax to declare a new group for your commands. - This change also remove the automatic listing of all namespace commands upon a typo or unknown/wrong command. * Fixes #224: DeprecationWarning makes tests fail with python 3.6.2 0.22 ---- * Fixes #223: 'gandi setup' command error * Fixes #222: AttributeError during vm creation on a private vlan * Fixes tests for 'gandi deploy' and 'gandi status' commands 0.21 ---- * Add new 'gandi disk migrate' command * Update 'gandi setup' command to ask for apikey for REST API * Handle deprecated images - Add a warning during 'gandi vm create' command - Display a * before image labels on 'gandi vm create' help - Display a /!\ DEPRECATED on 'gandi vm images' command * Fixes #220: gandi record update issues - Do not cast to int the id of the record, use the retrieve value - Handle both record syntax with 'IN' or not when parsing - Delete created zone if record.update call fail from xmlrpc API * Fixes #219: Can't remove disk snapshot profile * vm: delete: Fix delete when we reach the list limit - Fixed a bug when deleting a vm that wasn't listed in the first 500 results of gandi.iaas.list. * Fix issue when updating disk kernel with a kernel from another datacenter - CLI was proposing only kernels available on datacenter 1, but some kernels are available only on other datacenters, so we list everything for --kernel parameters, and for disk update command we add a new check if this kernel is available for this disk on this datacenter. * Add epilog to help messages to notify user about man documentation * Add one new verbose level for dumping data 0.20 ---- * Add support for python3.6 * Debian 8 is the new default VM image * FR-SD3 is the new default datacenter * Update 'gandi mail create' command to allow passing password as parameter * Update 'gandi certificate create' command: duration is now limited to 2 years * Update 'gandi ip create' command to fix bad units in help message * Fixes #182: 'gandi disk create' will detect datacenter when creating a new VM disk * Fixes #184: 'gandi disk list' can now filter for attach/detach state * Fixes #192: 'gandi certificate info' now still works after 500 certificates * Fixes #201: 'gandi certificate export' was duplicating intermediate certificate * Fixes #211: 'gandi paas deploy' tests should work again when using git commands * Fixes a bug with options not using corrected value when deprecated * Update unixpipe module to remove usage of posix and non portable imports 0.19 ---- * Update create commands for namespaces: vm, paas, ip, disk, vlan, webacc to handle new datacenter status: - prevent using a closed datacenter for creation - display a warning when using a datacenter which will be closed in the future * Update 'gandi mailbox info' command: aliases are now sorted * Fixes #178: 'gandi account info' command now display prepaid amount * Fixes #185: 'gandi domain create' command can now change nameservers * Fixes #187: 'gandi record list' command has a --limit parameter * Fixes #188: broken links in README * Fixes certificate unittest for python3 0.18 ---- * Update 'gandi paas update' command: --upgrade parameter is now a boolean flag * Update 'gandi deploy' command: - new '--remote' and '--branch' options - better handling of case when git configuration is not configured as expected - will try and use the gandi remote by default to extract deploy url - will deploy the branch master by default - will fallback to guessing the Simple Hosting remote from git configuration of the branch to deploy - improve error message when unable to execute * Update VM spin up timeout to 5min (from 2min) for bigger VM. * Add more unittests. 0.17 ---- * Gandi CLI now supports python3.5 * Update 'gandi paas' namespace: - Add new command 'gandi paas attach' to add an instance vhost's git remote to local git repository. - Update 'gandi deploy' command: - don't need a local configuration file anymore - need to be called on attached paas instance - Update 'gandi paas clone' command: - you can now specify which vhost and local directory to use - Use correct prefix for name generation in create command * Convert 'gandi config' command to a namespace to allow configuration display and edition * Fixes bug with 'gandi account' command which was broken sometimes * Fixes a bug with 'gandi vlan update' command when using --create flag * Fixes a bug with mail alias update when using same number of alias add/del parameters. * Fixes a bug when using a resource name and having more than 100 items of this resource type * Fixes size parameter choices for 'gandi paas create' command. * Fixes bug with 'gandi record update' command and argument parsing * Fixes bug with 'gandi record' commands: - must always exit if wrong/missing input parameter. * Always display CLI full help message when requesting an unknown command * Be less aggressive when trying to connect via SSH during 'gandi vm create' * Better handling of no hosting credits error. * Add more unittests. * Fixes #108 * Fixes #128 * Fixes #140 * Fixes #157 * Fixes #161 * Fixes #165 * Fixes #170 * Fixes #173 0.16 ---- * Update parameter '--datacenter': - allow dc_code as optional value - old values: FR/LU/US are still working so it doesn't break compatibility but they will be deprecated in next releases * Update output of IP creation to display IP address: - for 'gandi ip create' command - for 'gandi vm create' command with --ip option * Various improvements to modules for library usage: - datacenter - account - domain - operations * Update 'gandi mail info' command: - change output of responder and quota information to be more user friendly * Update click requirement version to >= 3.1 so we always use the latest version * Fixes debian python3 packaging * Fixes #148 * Fixes #147 0.15 ---- * New command 'gandi domain renew' command to renew a domain. * Update 'domain info' command: - add creation, update and expiration date to output - changes nameservers and services output for easier parsing * Update 'gandi domain create' command: - the domain name can now be passed as argument, the option --domain will be deprecated upon next release. * Update 'gandi disk update' command: - add new option '--delete-snapshotprofile' to remove a snapshot profile from disk * Update 'gandi ip delete' command: - now accept multiple IP as argument in order to delete a list of IP addresses * Fixes #119 * Fixes #129 * Fixes #141 0.14 ---- * New 'certstore' namespace to manage certificates in webaccs. * New command 'gandi vhost update' to activate ssl on the vhost. * Update 'gandi vhost create' and 'gandi vhost update' commands to handle hosted certificates. * Update 'gandi paas create' command to handle hosted certificates. * Update 'gandi webacc create' and add to handle hosted certificates. * Update 'gandi paas info' command: - add new --stat parameter, which will display cached page statistic based on the last 24 hours. - add snapshotprofile information to output. * Update 'gandi oper list' command to add filter on step type. * Update 'gandi paas update' command to allow deleting an existing snapshotprofile. * Update 'gandi status' command to also display current incidents not attached to a specific service. * Fixes #132 * Fixes #131 * Fixes #130 * Fixes #120 * Fixes error message when API is not reachable. 0.13 ---- * New 'webacc' namespace for managing web accelerators for virtual machines. * New command 'gandi status' to display Gandi services statuses. * New command 'gandi ip update' to update reverse (PTR record) * Update 'gandi vm create' command to add new parameter --ssh to open a SSH session to the machine after creation is complete. This means that the previous behavior is changed and vm creation will not automatically open a session anymore. * Update several commands with statistics information: - add disk quota usage in 'gandi paas info' command - add disk network and vm network stats in 'gandi vm info' command * Update 'gandi account info' command to display credit usage per hour * Update 'gandi certificate update' command to displays how to follow and retrieve the certificate after completing the process. * Update 'gandi ip info' command to display reverse information * Update 'gandi ip list' command to add vlan filtering * Update 'gandi vm list' command to add datacenter filtering * Update 'gandi vm create' command to allow usage of a size suffix for --size parameter (as in disk commands) * Update 'gandi vm ssh' command to add new parameter --wait to wait for * Update 'certificate' namespace: - 'gandi certificate follow' command to know in which step of the process is the current operation - 'gandi certificate packages' display has been enhanced - 'gandi certificate create' will try to guess the number of altnames or wildcard - 'gandi certificate export' will retrieve the correct intermediate certificate. * Update 'gandi disk attach' command to enable mounting in read-only and also specify position where disk should be attached. * Update 'gandi record list' command with new parameter --format * Update 'gandi record update' command to update only one record in the zone file * Update 'gandi vm list' command to add datacenter filtering * Refactor code for 'gandi ip attach' and 'gandi ip delete' commands virtual machine sshd to come up (timeout 2min). * Refactor 'gandi vm create' command to pass the script directly to the API and not use scp manually after creation. * Fixes wording and various typos in documentation and help pages. * Add more unittests. * Add tox and httpretty to tests packages requirements for unittests 0.12 ---- * New 'ip' namespace with commands for managing public/private ip resources. * New 'vlan' namespace with commands for managing vlans for virtual machines. * New command 'gandi account info' to display information about credits amount for hosting account. * New command 'gandi contact create' to create a new contact. * New command 'gandi disk snapshot' to create a disk snapshot on the fly. * Update 'gandi vm create' command: - enabling creation of vlan and ip assignment for this vlan directly during vm creation. - enabling creation of a private only ip virtual machine. - parameter --ip-version is not read from configuration file anymore, still defaulting to 4. * Update 'gandi paas create' command to allow again the use of password provided on the command line. * Update 'record' namespace to add delete/update commands, with option to export zones to file. * Use different prefix for temporary names based on type of resource. * Switch to use HVM image as default disk image when creating virtual machine. * Add kernel information to output of 'gandi disk list' command. * Fixes bug with paas vhost directory creation. * Fixes bug with 'gandi mail delete' command raising a traceback. * Fixes bug with duplicates entries in commands accepting multiple resources. * Fixes various typos in documentation and help pages. * Add first batch of unittests. 0.11 ---- * New command 'gandi disk detach' to detach disks from currently attached vm. * New command 'gandi disk attach' to attach disk to a vm. * New command 'gandi disk rollback' to perform a rollback from a snapshot. * New parameter --source for command 'gandi disk create' to allow creation of a new disk from an existing disk or snapshot. * New parameter --script for command 'gandi vm create' to allow upload of a local script on freshly created vm to be run after creation is completed. * Update parameter --size of 'gandi disk create/update' command to accept optionnal suffix: M,G,T (from megabytes up to terabytes). * Update command 'gandi vm ssh' to accept args to be passed to launched ssh command. * Fixes bug with 'gandi vm create' command and image parameter, which failed when having more than 100 disks in account. * Fixes bug with 'gandi paas info' command to display sftp_server url. * Fixes bug with 'gandi record list' command when requesting a domain not managed at Gandi. * Rename --sshkey parameter of 'gandi sshkey create' command to --filename. * Prettify output of list/info commands. * GANDI_CONFIG environment variable can be used to override the global configuration file. * Bump click requirement version to <= 4. 0.10 ---- * Add new dependency to request library, for certificate validation during xmlrpc calls. * New command 'gandi vm kernels' to list available kernels, can also be used to filter by vm to know which kernel is compatible. * New parameters --cmdline and --kernels for command 'gandi disk update' to enable updating of cmdline and/or kernel. * New parameter --size for command 'gandi vm create' to specify disk size during vm creation. * Handle max_memory setting in command 'gandi vm update' when updating memory. New parameter --reboot added to accept a VM reboot for non-live update. * Update command 'gandi vm images' to also display usable disks as image for vm creation. * Security: validate server certificate using request as xmlrpc transport. * Security: restrict configuration file rights to owner only. * Refactor code of custom parameters, to only query API when needed, improving overall speed of all commands. * Fixes bug with sshkey parameter for 'gandi paas create' and 'gandi paas update' commands. * When an API call fail, we can call again using dry-run flag to get more explicit errors. Used by 'gandi vhost create' command. * Allow Gandi CLI to load custom modules using 'GANDICLI_PATH' environment variable, was previously only done by commands. 0.9 --- * New command 'gandi docker' to manage docker instance. This requires a docker client to work. * Improve 'vm ssh' command to support identity file, login@ syntax. * Login is no longer a mandatory option and saved to configuration when creating a virtual machine. * Add short summary to output when creating a virtual machine. * Fixes bug when no sshkey available during setup. * Fixes bug with parameters validation when calling a command before having entered api credentials. 0.8 --- * New record namespace to manage domain zone record entries 0.7 --- * Add and update License information to use GPL-3 * Uniformize help strings during creation/deletion commands 0.6 --- * New mail namespace for managing mailboxes and aliases * New command 'disk create' to create a virtual disk * New command 'vm ssh' to open a ssh connection to an existing virtual machine * New command 'help' which behave like --help option. * Using 'gandi namespace' without full command will display list of available commands for this namespace and associated short help. * 'gandi paas create' and 'gandi vm create' commands now use sshkeys, and default to LU as default datacenter. 0.5 --- * Fixes Debian packaging 0.4 --- * Fixes bug with snapshotprofile list command preventing 'gandi setup' to work after clean installation * Allow Gandi CLI to load custom modules/commands using 'GANDICLI_PATH' environment variable 0.3 --- * New certificate namespace for managing certificates * New disk namespace for managing iaas disks * New snapshotprofile namespace to know which profiles exists * Allow override of configuration values for apikey, apienv and apihost using shell environment variables API_KEY, API_ENV, API_HOST. * Bugfixes on various vm and paas commands * Fixes typos in docstrings * Update man page 0.2 --- * New vhost namespace for managing virtual host for PaaS instances * New sshkey namespace for managing a sshkey keyring * Bugfixes on various vm and paas commands * Bugfixes when using a hostname using only numbers * Added a random unique name generated for temporary VM and PaaS 0.1 --- * Initial release gandi.cli-1.2/PKG-INFO0000644000175000017500000012274513227415174015200 0ustar sayounsayoun00000000000000Metadata-Version: 1.1 Name: gandi.cli Version: 1.2 Summary: Gandi command line interface Home-page: https://github.com/Gandi/gandi.cli Author: Gandi Author-email: feedback@gandi.net License: UNKNOWN Description: # Gandi CLI Use `$ gandi` to easily create and manage web resources from the command line. * `$ gandi domain` to buy and manage your domain names * `$ gandi paas` to create and deploy your web applications * `$ gandi vm` to spin up and upgrade your virtual machines * `$ gandi certificate` to manage your ssl certificates * `$ gandi` to list all available commands * [Detailed examples](#use-cases) * [All commands](#all-commands) ## Table of contents * [Requirements](#requirements) * [Installation](#installation) * [Getting started](#getting-started) * [Use cases](#use-cases) * [Registering a Domain Name](#registering-a-domain-name) * [Creating a Virtual Machine](#creating-a-virtual-machine) * [Deploying a Web Application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) * [Advanced Usage](#advanced-usage) * [All Commands](#all-commands) * [Build manpage](#build-manpage) * [Configuration](#configuration) * [Contributing](#contributing) * [Code status](#code-status) * [License](#license) ## Requirements * A compatible operating system (Linux, BSD, Mac OS X/Darwin, Windows) * Python 2.6/2.7/3.2/3.3/3.4/3.5/3.6 * openssl * openssh * git Recommended tools * [pip](https://pip.pypa.io/en/latest/installing.html) * [virtualenv](https://virtualenv.pypa.io/en/latest/installation.html) * docker ## Installation ### Install with pip and virtualenv $ virtualenv /some/directory/gandi.cli $ source /some/directory/gandi.cli/bin/activate $ pip install gandi.cli ### Build from source $ cd /path/to/the/repository $ python setup.py install --user ### From the Debian package $ ln -sf packages/debian debian && debuild -us -uc -b && echo "Bisou" ## Getting started Using our classic (V4) website: 1. To get started, you can create a [free Gandi account](https://v4.gandi.net/contact/create) and get your Gandi Handle 2. [Generate your Production API Token](https://v4.gandi.net/admin/api_key) from the account admin section 3. You may also want to [top-up your prepaid account](https://v4.gandi.net/prepaid) 4. To manipulate VM's, you also need to [purchase credits](https://www.gandi.net/credit/buy) (you can use funds from your prepaid account) Using our latest (V5) website: 1. To get started, you can create a [free Gandi account](https://account.gandi.net/en/create_account) and get your Gandi username 2. [Generate your Production API Token](https://account.gandi.net/en/) from within the account Security section 3. You may also want to [top-up your prepaid account](https://admin.gandi.net/billing/) 4. To manipulate VM's, you currently need to follow above steps to create an account on our classic (V4) website. Then run the setup $ gandi setup > API Key: x134z5x4c5c # copy-paste your api key > Environment [production] : # press enter for Production, the default > SSH key [~/.ssh/id_rsa.pub] : # your SSH public key for hosting instances and servers See the [Advanced Usage](#advanced-usage) section for more details on configuration. ## Use cases * [Registering a domain name](#registering-a-domain-name) * [Creating a virtual machine](#creating-a-virtual-machine) * [Deploying a web application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) ### Registering a Domain Name Gandi is a domain name registrar since 1999. The oldest in France and one of the world's leading, Gandi is recognized for its No Bullshit™ trademark and approach to domain names. You can now buy and manage domains in any of the 500+ TLD's that Gandi offers from the command line. [Know more about Gandi Domains on the website](https://www.gandi.net/domain). #### 1. Buy a domain using the interactive prompt $ gandi domain create > Domain: example.com # enter the domain name here > example.com is available > Duration [1] : 1 # enter the duration in years This will create a domain and use your default information for Ownership, Admin, Technical and Billing info. #### 2. Buy a domain in one line $ gandi domain create --domain example.com --duration 1 #### 3. Buy a domain with custom contacts $ gandi domain create --domain example.com --duration 1 --owner XYZ123-GANDI --admin XYZ123-GANDI --tech XYZ123-GANDI --bill XYZ123-GANDI You can use the information of Gandi handles associated to Contacts in your account to setup Owner, Admin, Technical and Billing info. #### 3. List your domains $ gandi domain list #### 4. Get information about a domain $ gandi domain info example.com #### 5. Manage NS records for your domains ##### Create a new record $ gandi record create example.com --name www --type A --value 127.0.0.1 Add a new record to the domain's current zone file and activate it. ##### List your records $ gandi record list example.com List a domain's zone file records. You can use the `--format` parameter to change the output format to `text` or `json`. ##### Update one record $ gandi record update example.com --record "@ 3600 IN A 127.0.0.1" --new-record "@ 3600 IN A 0.0.0.0" This command is useful to update only one record at the time. The pattern to use is `name TTL CLASS TYPE value`. You can easily check or copy-paste the values you need to replace using the `--format text` parameter: $ gandi record list example.com --format text ##### Update many records $ gandi record list example.com --format text > file.zone Use this command to extract your zone records into a file called `file.zone` (or something else). Simply edit the file to your liking and then update the entire zone file with it. $ gandi record update example.com -f file.zone ##### Delete records $ gandi record delete example.com --value 127.0.0.1 Delete all records that match the given parameters from a domain's zone file. In this example, if there were many records with '127.0.0.1' as their value, all of them would be deleted. ### Creating a Virtual Machine Gandi Server offers powerful Xen- and Linux-based virtual machines since 2007. Virtual machines can be configured and upgraded on the fly to your liking. For example, you can start with 1GB of RAM, and run a command to add 2GB of RAM and 2 CPUs without even having to restart it. Gandi Server measures consumption by the hour and uses a prepaid credit system. To learn more, [check out the Gandi Server website](https://www.gandi.net/hosting/server/). #### 1. Create and access a VM $ gandi vm create * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 1 cores, 256Mb memory, ip v4+v6, image Debian 8, hostname: temp1415183684, datacenter: LU Create a virtual machine with the default configuration and a random hostname. #### 2. Upgrade a VM $ gandi vm update temp1415183684 --memory 2048 --cores 2 Set the VM's RAM to 2GB and add a CPU core on the fly. #### 3. Create a custom VM $ gandi vm create --datacenter US --hostname docker --cores 2 --memory 3072 --size 10240 --image "Ubuntu 14.04 64 bits LTS (HVM)" --run "curl -sSL https://get.docker.com/ubuntu/ | sh" * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 2 cores, 3072Mb memory, ip v4+v6, image Ubuntu 14.04 64 bits LTS, hostname: docker, datacenter: LU This command will setup the above VM, and install docker by running `curl -sSL https://get.docker.com/ubuntu/ | sh` after creation. #### 4. View your resources $ gandi vm list #### 5. Get all the details about a VM $ gandi vm info docker ### Deploying a Web Application with Simple Hosting Gandi Simple Hosting is a PaaS (Platform as a Service) offering fast code deployment and easy scaling, powering over 50,000 apps since its inception in 2012. Instances can run apps in 4 languages (PHP, Python, Node.js and Ruby) along with one of 3 popular databases (MySQL, PostgreSQL and MongoDB) and operate on a managed platform with built-in http caching. Plans cover all scales, from small to world-class projects. [Check out the website for more information](https://www.gandi.net/hosting/simple). #### 1. Create a Simple Hosting instance $ gandi paas create --name myapp --type nodejspgsql --size S --datacenter FR --duration 1 #### 2. Attach and push to your instance's git repository Simple Hosting offers two "modes": the **App mode**, where an instance offers a single git repository (`default.git`) and the **Sites mode**, where you can have multiple git repositories per instance (one for each VHOST, for example `www.myapp.com.git`). Node.js, Python and Ruby instances run in App mode, whereas PHP instances run in Sites mode by default. Note: If you create a wildcard VHOST for your PHP instance, the App mode will be activated. Assuming you have local directory called `app` where you have placed your code base, you can use the following commands to create a git remote (called "gandi" by default) and push your code. $ cd app $ gandi paas attach myapp # App mode $ gandi paas attach myapp --vhost www.myapp.com # Sites mode $ git push gandi master #### 3. Deploy your code Still inside the `app` folder, you can use the following command to start the deploy process, which will checkout your code, install dependencies and launch (or relaunch) the app process: $ gandi deploy ### Creating a SSL Certificate Gandi SSL offers a range of SSL certificates to help you secure your projects. You can order, obtain, update and revoke your certificates from the command line. #### 1. Find the right plan for you $ gandi certificate plans Our Standard, Pro and Business plans offer different validation methods and guarantees. Each plan supports all or some of these types of certificates: single address, wildcard and/or multiple subdomains. To discover our offering and find the right certificate for your project, [compare our plans](https://www.gandi.net/ssl/compare) and [try our simulator](https://www.gandi.net/ssl/which-ssl-certificate). Gandi CLI can choose the right certificate type for you depending on the number of domains (altnames) you supply at creation time. You only need to set it if you plan on adding more domains to the certificate in the future. #### 2. Create the Certificate WARNING : This command is billable. To request a certificate, you need to use a private key to generate and sign a CSR (Certificate Signing Request) that will be supplied to Gandi. The `create` command will take care of this for you if you don't have them already, or you can supply your CSR directly. Check out the examples below or [our wiki](http://wiki.gandi.net/ssl) for more information on how SSL certificates work. To create a single domain Standard certificate: $ gandi certificate create --cn "domain.tld" For a wildcard Standard certificate: $ gandi certificate create --cn "*.domain.tld" For a multi domain Standard certificate: $ gandi certificate create --cn "*.domain.tld" --altnames "host1.domain.tld" --altnames "host2.domain.tld" You can also specify a plan type. For example, for a single domain Business certificate: $ gandi certificate create --cn "domain.tld" --type "bus" If you have a CSR (you can give the CSR content or the path): $ gandi certificate create --csr /path/to/csr/file #### 3. Follow the Certificate create operation $ gandi certificate follow #### 4. Get the Certificate As soon as the operation is DONE, you can export the certificate. $ gandi certificate export "domain.tld" You can also retrieve intermediate certificates if needed. $ gandi certificate export "domain.tld" --intermediate Find information on how to use your certificate with different servers on [our wiki](http://wiki.gandi.net/en/ssl). ### Adding a Web Application vhost with SSL Gandi allow you to associate a certificate with your vhost. #### 1. You already have the matching certificate at Gandi Just create the vhost giving it the private key used to generate that certificate. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" #### 2. You have the matching certificate but not at Gandi (or in another account) Declare the hosted certificate. $ gandi certstore create --pk "domain.tld.key" --crt "domain.tld.crt" And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" --ssl #### 3. You don't have any certificate and plan to get it at Gandi Create the certificate. $ gandi certificate create --cn "domain.tld.key" --type std And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" ### Creating a private VLAN You can use Gandi CLI to create and setup your private VLANs. For more detailed information on how VLANs and networking in general works at Gandi, please check out our resources: * [Creating a private VLAN with Gandi CLI](http://wiki.gandi.net/en/tutorials/cli/pvlan) * [VLAN on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network/pvlan) * [Networking on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network) #### Create a VLAN $ gandi vlan create --name my-vlan-in-lu --datacenter LU \ --subnet "192.168.1.0/24" --gateway 192.168.1.1 To create a VLAN you need to determine its `name` and `datacenter`. You can also set the `subnet` at creation time, or a default subnet will be chosen for you. The `gateway` setting is also optional and you can update both of these settings at any moment. $ gandi vlan update my-vlan-in-lu --gateway 192.168.1.254 #### Attach an existing VM to a VLAN To add an existing VM to a VLAN, you can create a private network interface and attach it to the VM. $ gandi ip create --vlan my-vlan-in-lu --attach my-existing-vm --ip 192.168.1.254 If you don't specify the IP you want to use, one will be chosen for you from the VLAN's subnet. #### Create a "Private VM" In fact there's no such thing as a "Private VM", but you can create a VM and only attach a private interface to it. $ gandi vm create --hostname my-private-vm --vlan my-vlan-in-lu --ip 192.168.1.2 Please note that a private VM cannot be accessed through the emergency console. You'll need a public VM that also has a private interface on the same VLAN to gain access. You can check out [our tutorial](http://wiki.gandi.net/en/tutorials/cli/pvlan) for an example of how to achieve this. #### More options $ gandi vlan --help Use the `--help` flag for more VLAN management options. ## Advanced Usage ### All Commands To list all available commands, type `$ gandi --help` For extended instructions, check out the `man` page. ### Build manpage Install python-docutils and run: $ rst2man --no-generator gandicli.man.rst > gandi.1.man Then to read the manpage: $ man ./gandi.1.man ### Configuration Run `$ gandi setup` to configure your settings (see [Getting started](#getting-started)) Use `$ gandi config` to set and edit custom variables. The default variables are: * `sshkey` # path to your public ssh key * `api.host` # the URL of the API endpoint to use (i.e OTE or Production) * `api.key` # the relevant API key for the chosen endpoint ## Contributing We <3 contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. You can check the [contributors list](https://github.com/Gandi/gandi.cli/graphs/contributors). ## Code status [![Build Status](https://travis-ci.org/Gandi/gandi.cli.svg?branch=master)](https://travis-ci.org/Gandi/gandi.cli) [![Coverage Status](https://coveralls.io/repos/Gandi/gandi.cli/badge.svg?branch=master)](https://coveralls.io/r/Gandi/gandi.cli?branch=master) [![Pip Version](https://img.shields.io/pypi/v/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Python Version](https://img.shields.io/pypi/pyversions/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Download Stat](https://img.shields.io/pypi/dm/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) ## License / Copying Copyright © 2014-2018 Gandi S.A.S Gandi CLI is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gandi CLI is distributed in the hope that it will be useful, but WITHOUT 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 Gandi CLI. If not, see . Changelog ========= 1.2 --- * Add support for paas size s+ for creation/update * Fixes #232: Update 'gandi record update' command to allow filtering by name * Fix bug when attempting to migrate a vm which cannot be migrated * Only display DC closed warning if a date is set 1.1 --- * FR-SD5 is now the default datacenter. * Add new 'gandi dns update' command. * Fixes #228: Generate a user password at the creation of a VM * Improve wait for ssh connectivity after 'gandi vm create' command to handle ipv6 * Fix a bug with 'gandi disk migrate' command not working with multiple datacenters choices * Improve documentation for generating username/apikey with Gandi V5 1.0 ---- * New 'dns' namespace to manage DNS records/dnssec through LiveDNS API. * Add new 'gandi vm migrate' command. * Refactor internal click code usage. Remove hackish code to handle nested commands which was limited to only 1 nested level. - This change will break code of users which were using custom commands on top of Gandi CLI, To fix this you have to use the proper click syntax to declare a new group for your commands. - This change also remove the automatic listing of all namespace commands upon a typo or unknown/wrong command. * Fixes #224: DeprecationWarning makes tests fail with python 3.6.2 0.22 ---- * Fixes #223: 'gandi setup' command error * Fixes #222: AttributeError during vm creation on a private vlan * Fixes tests for 'gandi deploy' and 'gandi status' commands 0.21 ---- * Add new 'gandi disk migrate' command * Update 'gandi setup' command to ask for apikey for REST API * Handle deprecated images - Add a warning during 'gandi vm create' command - Display a * before image labels on 'gandi vm create' help - Display a /!\ DEPRECATED on 'gandi vm images' command * Fixes #220: gandi record update issues - Do not cast to int the id of the record, use the retrieve value - Handle both record syntax with 'IN' or not when parsing - Delete created zone if record.update call fail from xmlrpc API * Fixes #219: Can't remove disk snapshot profile * vm: delete: Fix delete when we reach the list limit - Fixed a bug when deleting a vm that wasn't listed in the first 500 results of gandi.iaas.list. * Fix issue when updating disk kernel with a kernel from another datacenter - CLI was proposing only kernels available on datacenter 1, but some kernels are available only on other datacenters, so we list everything for --kernel parameters, and for disk update command we add a new check if this kernel is available for this disk on this datacenter. * Add epilog to help messages to notify user about man documentation * Add one new verbose level for dumping data 0.20 ---- * Add support for python3.6 * Debian 8 is the new default VM image * FR-SD3 is the new default datacenter * Update 'gandi mail create' command to allow passing password as parameter * Update 'gandi certificate create' command: duration is now limited to 2 years * Update 'gandi ip create' command to fix bad units in help message * Fixes #182: 'gandi disk create' will detect datacenter when creating a new VM disk * Fixes #184: 'gandi disk list' can now filter for attach/detach state * Fixes #192: 'gandi certificate info' now still works after 500 certificates * Fixes #201: 'gandi certificate export' was duplicating intermediate certificate * Fixes #211: 'gandi paas deploy' tests should work again when using git commands * Fixes a bug with options not using corrected value when deprecated * Update unixpipe module to remove usage of posix and non portable imports 0.19 ---- * Update create commands for namespaces: vm, paas, ip, disk, vlan, webacc to handle new datacenter status: - prevent using a closed datacenter for creation - display a warning when using a datacenter which will be closed in the future * Update 'gandi mailbox info' command: aliases are now sorted * Fixes #178: 'gandi account info' command now display prepaid amount * Fixes #185: 'gandi domain create' command can now change nameservers * Fixes #187: 'gandi record list' command has a --limit parameter * Fixes #188: broken links in README * Fixes certificate unittest for python3 0.18 ---- * Update 'gandi paas update' command: --upgrade parameter is now a boolean flag * Update 'gandi deploy' command: - new '--remote' and '--branch' options - better handling of case when git configuration is not configured as expected - will try and use the gandi remote by default to extract deploy url - will deploy the branch master by default - will fallback to guessing the Simple Hosting remote from git configuration of the branch to deploy - improve error message when unable to execute * Update VM spin up timeout to 5min (from 2min) for bigger VM. * Add more unittests. 0.17 ---- * Gandi CLI now supports python3.5 * Update 'gandi paas' namespace: - Add new command 'gandi paas attach' to add an instance vhost's git remote to local git repository. - Update 'gandi deploy' command: - don't need a local configuration file anymore - need to be called on attached paas instance - Update 'gandi paas clone' command: - you can now specify which vhost and local directory to use - Use correct prefix for name generation in create command * Convert 'gandi config' command to a namespace to allow configuration display and edition * Fixes bug with 'gandi account' command which was broken sometimes * Fixes a bug with 'gandi vlan update' command when using --create flag * Fixes a bug with mail alias update when using same number of alias add/del parameters. * Fixes a bug when using a resource name and having more than 100 items of this resource type * Fixes size parameter choices for 'gandi paas create' command. * Fixes bug with 'gandi record update' command and argument parsing * Fixes bug with 'gandi record' commands: - must always exit if wrong/missing input parameter. * Always display CLI full help message when requesting an unknown command * Be less aggressive when trying to connect via SSH during 'gandi vm create' * Better handling of no hosting credits error. * Add more unittests. * Fixes #108 * Fixes #128 * Fixes #140 * Fixes #157 * Fixes #161 * Fixes #165 * Fixes #170 * Fixes #173 0.16 ---- * Update parameter '--datacenter': - allow dc_code as optional value - old values: FR/LU/US are still working so it doesn't break compatibility but they will be deprecated in next releases * Update output of IP creation to display IP address: - for 'gandi ip create' command - for 'gandi vm create' command with --ip option * Various improvements to modules for library usage: - datacenter - account - domain - operations * Update 'gandi mail info' command: - change output of responder and quota information to be more user friendly * Update click requirement version to >= 3.1 so we always use the latest version * Fixes debian python3 packaging * Fixes #148 * Fixes #147 0.15 ---- * New command 'gandi domain renew' command to renew a domain. * Update 'domain info' command: - add creation, update and expiration date to output - changes nameservers and services output for easier parsing * Update 'gandi domain create' command: - the domain name can now be passed as argument, the option --domain will be deprecated upon next release. * Update 'gandi disk update' command: - add new option '--delete-snapshotprofile' to remove a snapshot profile from disk * Update 'gandi ip delete' command: - now accept multiple IP as argument in order to delete a list of IP addresses * Fixes #119 * Fixes #129 * Fixes #141 0.14 ---- * New 'certstore' namespace to manage certificates in webaccs. * New command 'gandi vhost update' to activate ssl on the vhost. * Update 'gandi vhost create' and 'gandi vhost update' commands to handle hosted certificates. * Update 'gandi paas create' command to handle hosted certificates. * Update 'gandi webacc create' and add to handle hosted certificates. * Update 'gandi paas info' command: - add new --stat parameter, which will display cached page statistic based on the last 24 hours. - add snapshotprofile information to output. * Update 'gandi oper list' command to add filter on step type. * Update 'gandi paas update' command to allow deleting an existing snapshotprofile. * Update 'gandi status' command to also display current incidents not attached to a specific service. * Fixes #132 * Fixes #131 * Fixes #130 * Fixes #120 * Fixes error message when API is not reachable. 0.13 ---- * New 'webacc' namespace for managing web accelerators for virtual machines. * New command 'gandi status' to display Gandi services statuses. * New command 'gandi ip update' to update reverse (PTR record) * Update 'gandi vm create' command to add new parameter --ssh to open a SSH session to the machine after creation is complete. This means that the previous behavior is changed and vm creation will not automatically open a session anymore. * Update several commands with statistics information: - add disk quota usage in 'gandi paas info' command - add disk network and vm network stats in 'gandi vm info' command * Update 'gandi account info' command to display credit usage per hour * Update 'gandi certificate update' command to displays how to follow and retrieve the certificate after completing the process. * Update 'gandi ip info' command to display reverse information * Update 'gandi ip list' command to add vlan filtering * Update 'gandi vm list' command to add datacenter filtering * Update 'gandi vm create' command to allow usage of a size suffix for --size parameter (as in disk commands) * Update 'gandi vm ssh' command to add new parameter --wait to wait for * Update 'certificate' namespace: - 'gandi certificate follow' command to know in which step of the process is the current operation - 'gandi certificate packages' display has been enhanced - 'gandi certificate create' will try to guess the number of altnames or wildcard - 'gandi certificate export' will retrieve the correct intermediate certificate. * Update 'gandi disk attach' command to enable mounting in read-only and also specify position where disk should be attached. * Update 'gandi record list' command with new parameter --format * Update 'gandi record update' command to update only one record in the zone file * Update 'gandi vm list' command to add datacenter filtering * Refactor code for 'gandi ip attach' and 'gandi ip delete' commands virtual machine sshd to come up (timeout 2min). * Refactor 'gandi vm create' command to pass the script directly to the API and not use scp manually after creation. * Fixes wording and various typos in documentation and help pages. * Add more unittests. * Add tox and httpretty to tests packages requirements for unittests 0.12 ---- * New 'ip' namespace with commands for managing public/private ip resources. * New 'vlan' namespace with commands for managing vlans for virtual machines. * New command 'gandi account info' to display information about credits amount for hosting account. * New command 'gandi contact create' to create a new contact. * New command 'gandi disk snapshot' to create a disk snapshot on the fly. * Update 'gandi vm create' command: - enabling creation of vlan and ip assignment for this vlan directly during vm creation. - enabling creation of a private only ip virtual machine. - parameter --ip-version is not read from configuration file anymore, still defaulting to 4. * Update 'gandi paas create' command to allow again the use of password provided on the command line. * Update 'record' namespace to add delete/update commands, with option to export zones to file. * Use different prefix for temporary names based on type of resource. * Switch to use HVM image as default disk image when creating virtual machine. * Add kernel information to output of 'gandi disk list' command. * Fixes bug with paas vhost directory creation. * Fixes bug with 'gandi mail delete' command raising a traceback. * Fixes bug with duplicates entries in commands accepting multiple resources. * Fixes various typos in documentation and help pages. * Add first batch of unittests. 0.11 ---- * New command 'gandi disk detach' to detach disks from currently attached vm. * New command 'gandi disk attach' to attach disk to a vm. * New command 'gandi disk rollback' to perform a rollback from a snapshot. * New parameter --source for command 'gandi disk create' to allow creation of a new disk from an existing disk or snapshot. * New parameter --script for command 'gandi vm create' to allow upload of a local script on freshly created vm to be run after creation is completed. * Update parameter --size of 'gandi disk create/update' command to accept optionnal suffix: M,G,T (from megabytes up to terabytes). * Update command 'gandi vm ssh' to accept args to be passed to launched ssh command. * Fixes bug with 'gandi vm create' command and image parameter, which failed when having more than 100 disks in account. * Fixes bug with 'gandi paas info' command to display sftp_server url. * Fixes bug with 'gandi record list' command when requesting a domain not managed at Gandi. * Rename --sshkey parameter of 'gandi sshkey create' command to --filename. * Prettify output of list/info commands. * GANDI_CONFIG environment variable can be used to override the global configuration file. * Bump click requirement version to <= 4. 0.10 ---- * Add new dependency to request library, for certificate validation during xmlrpc calls. * New command 'gandi vm kernels' to list available kernels, can also be used to filter by vm to know which kernel is compatible. * New parameters --cmdline and --kernels for command 'gandi disk update' to enable updating of cmdline and/or kernel. * New parameter --size for command 'gandi vm create' to specify disk size during vm creation. * Handle max_memory setting in command 'gandi vm update' when updating memory. New parameter --reboot added to accept a VM reboot for non-live update. * Update command 'gandi vm images' to also display usable disks as image for vm creation. * Security: validate server certificate using request as xmlrpc transport. * Security: restrict configuration file rights to owner only. * Refactor code of custom parameters, to only query API when needed, improving overall speed of all commands. * Fixes bug with sshkey parameter for 'gandi paas create' and 'gandi paas update' commands. * When an API call fail, we can call again using dry-run flag to get more explicit errors. Used by 'gandi vhost create' command. * Allow Gandi CLI to load custom modules using 'GANDICLI_PATH' environment variable, was previously only done by commands. 0.9 --- * New command 'gandi docker' to manage docker instance. This requires a docker client to work. * Improve 'vm ssh' command to support identity file, login@ syntax. * Login is no longer a mandatory option and saved to configuration when creating a virtual machine. * Add short summary to output when creating a virtual machine. * Fixes bug when no sshkey available during setup. * Fixes bug with parameters validation when calling a command before having entered api credentials. 0.8 --- * New record namespace to manage domain zone record entries 0.7 --- * Add and update License information to use GPL-3 * Uniformize help strings during creation/deletion commands 0.6 --- * New mail namespace for managing mailboxes and aliases * New command 'disk create' to create a virtual disk * New command 'vm ssh' to open a ssh connection to an existing virtual machine * New command 'help' which behave like --help option. * Using 'gandi namespace' without full command will display list of available commands for this namespace and associated short help. * 'gandi paas create' and 'gandi vm create' commands now use sshkeys, and default to LU as default datacenter. 0.5 --- * Fixes Debian packaging 0.4 --- * Fixes bug with snapshotprofile list command preventing 'gandi setup' to work after clean installation * Allow Gandi CLI to load custom modules/commands using 'GANDICLI_PATH' environment variable 0.3 --- * New certificate namespace for managing certificates * New disk namespace for managing iaas disks * New snapshotprofile namespace to know which profiles exists * Allow override of configuration values for apikey, apienv and apihost using shell environment variables API_KEY, API_ENV, API_HOST. * Bugfixes on various vm and paas commands * Fixes typos in docstrings * Update man page 0.2 --- * New vhost namespace for managing virtual host for PaaS instances * New sshkey namespace for managing a sshkey keyring * Bugfixes on various vm and paas commands * Bugfixes when using a hostname using only numbers * Added a random unique name generated for temporary VM and PaaS 0.1 --- * Initial release Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Terminals Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) gandi.cli-1.2/README.md0000644000175000017500000004177713227374032015364 0ustar sayounsayoun00000000000000# Gandi CLI Use `$ gandi` to easily create and manage web resources from the command line. * `$ gandi domain` to buy and manage your domain names * `$ gandi paas` to create and deploy your web applications * `$ gandi vm` to spin up and upgrade your virtual machines * `$ gandi certificate` to manage your ssl certificates * `$ gandi` to list all available commands * [Detailed examples](#use-cases) * [All commands](#all-commands) ## Table of contents * [Requirements](#requirements) * [Installation](#installation) * [Getting started](#getting-started) * [Use cases](#use-cases) * [Registering a Domain Name](#registering-a-domain-name) * [Creating a Virtual Machine](#creating-a-virtual-machine) * [Deploying a Web Application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) * [Advanced Usage](#advanced-usage) * [All Commands](#all-commands) * [Build manpage](#build-manpage) * [Configuration](#configuration) * [Contributing](#contributing) * [Code status](#code-status) * [License](#license) ## Requirements * A compatible operating system (Linux, BSD, Mac OS X/Darwin, Windows) * Python 2.6/2.7/3.2/3.3/3.4/3.5/3.6 * openssl * openssh * git Recommended tools * [pip](https://pip.pypa.io/en/latest/installing.html) * [virtualenv](https://virtualenv.pypa.io/en/latest/installation.html) * docker ## Installation ### Install with pip and virtualenv $ virtualenv /some/directory/gandi.cli $ source /some/directory/gandi.cli/bin/activate $ pip install gandi.cli ### Build from source $ cd /path/to/the/repository $ python setup.py install --user ### From the Debian package $ ln -sf packages/debian debian && debuild -us -uc -b && echo "Bisou" ## Getting started Using our classic (V4) website: 1. To get started, you can create a [free Gandi account](https://v4.gandi.net/contact/create) and get your Gandi Handle 2. [Generate your Production API Token](https://v4.gandi.net/admin/api_key) from the account admin section 3. You may also want to [top-up your prepaid account](https://v4.gandi.net/prepaid) 4. To manipulate VM's, you also need to [purchase credits](https://www.gandi.net/credit/buy) (you can use funds from your prepaid account) Using our latest (V5) website: 1. To get started, you can create a [free Gandi account](https://account.gandi.net/en/create_account) and get your Gandi username 2. [Generate your Production API Token](https://account.gandi.net/en/) from within the account Security section 3. You may also want to [top-up your prepaid account](https://admin.gandi.net/billing/) 4. To manipulate VM's, you currently need to follow above steps to create an account on our classic (V4) website. Then run the setup $ gandi setup > API Key: x134z5x4c5c # copy-paste your api key > Environment [production] : # press enter for Production, the default > SSH key [~/.ssh/id_rsa.pub] : # your SSH public key for hosting instances and servers See the [Advanced Usage](#advanced-usage) section for more details on configuration. ## Use cases * [Registering a domain name](#registering-a-domain-name) * [Creating a virtual machine](#creating-a-virtual-machine) * [Deploying a web application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) ### Registering a Domain Name Gandi is a domain name registrar since 1999. The oldest in France and one of the world's leading, Gandi is recognized for its No Bullshit™ trademark and approach to domain names. You can now buy and manage domains in any of the 500+ TLD's that Gandi offers from the command line. [Know more about Gandi Domains on the website](https://www.gandi.net/domain). #### 1. Buy a domain using the interactive prompt $ gandi domain create > Domain: example.com # enter the domain name here > example.com is available > Duration [1] : 1 # enter the duration in years This will create a domain and use your default information for Ownership, Admin, Technical and Billing info. #### 2. Buy a domain in one line $ gandi domain create --domain example.com --duration 1 #### 3. Buy a domain with custom contacts $ gandi domain create --domain example.com --duration 1 --owner XYZ123-GANDI --admin XYZ123-GANDI --tech XYZ123-GANDI --bill XYZ123-GANDI You can use the information of Gandi handles associated to Contacts in your account to setup Owner, Admin, Technical and Billing info. #### 3. List your domains $ gandi domain list #### 4. Get information about a domain $ gandi domain info example.com #### 5. Manage NS records for your domains ##### Create a new record $ gandi record create example.com --name www --type A --value 127.0.0.1 Add a new record to the domain's current zone file and activate it. ##### List your records $ gandi record list example.com List a domain's zone file records. You can use the `--format` parameter to change the output format to `text` or `json`. ##### Update one record $ gandi record update example.com --record "@ 3600 IN A 127.0.0.1" --new-record "@ 3600 IN A 0.0.0.0" This command is useful to update only one record at the time. The pattern to use is `name TTL CLASS TYPE value`. You can easily check or copy-paste the values you need to replace using the `--format text` parameter: $ gandi record list example.com --format text ##### Update many records $ gandi record list example.com --format text > file.zone Use this command to extract your zone records into a file called `file.zone` (or something else). Simply edit the file to your liking and then update the entire zone file with it. $ gandi record update example.com -f file.zone ##### Delete records $ gandi record delete example.com --value 127.0.0.1 Delete all records that match the given parameters from a domain's zone file. In this example, if there were many records with '127.0.0.1' as their value, all of them would be deleted. ### Creating a Virtual Machine Gandi Server offers powerful Xen- and Linux-based virtual machines since 2007. Virtual machines can be configured and upgraded on the fly to your liking. For example, you can start with 1GB of RAM, and run a command to add 2GB of RAM and 2 CPUs without even having to restart it. Gandi Server measures consumption by the hour and uses a prepaid credit system. To learn more, [check out the Gandi Server website](https://www.gandi.net/hosting/server/). #### 1. Create and access a VM $ gandi vm create * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 1 cores, 256Mb memory, ip v4+v6, image Debian 8, hostname: temp1415183684, datacenter: LU Create a virtual machine with the default configuration and a random hostname. #### 2. Upgrade a VM $ gandi vm update temp1415183684 --memory 2048 --cores 2 Set the VM's RAM to 2GB and add a CPU core on the fly. #### 3. Create a custom VM $ gandi vm create --datacenter US --hostname docker --cores 2 --memory 3072 --size 10240 --image "Ubuntu 14.04 64 bits LTS (HVM)" --run "curl -sSL https://get.docker.com/ubuntu/ | sh" * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 2 cores, 3072Mb memory, ip v4+v6, image Ubuntu 14.04 64 bits LTS, hostname: docker, datacenter: LU This command will setup the above VM, and install docker by running `curl -sSL https://get.docker.com/ubuntu/ | sh` after creation. #### 4. View your resources $ gandi vm list #### 5. Get all the details about a VM $ gandi vm info docker ### Deploying a Web Application with Simple Hosting Gandi Simple Hosting is a PaaS (Platform as a Service) offering fast code deployment and easy scaling, powering over 50,000 apps since its inception in 2012. Instances can run apps in 4 languages (PHP, Python, Node.js and Ruby) along with one of 3 popular databases (MySQL, PostgreSQL and MongoDB) and operate on a managed platform with built-in http caching. Plans cover all scales, from small to world-class projects. [Check out the website for more information](https://www.gandi.net/hosting/simple). #### 1. Create a Simple Hosting instance $ gandi paas create --name myapp --type nodejspgsql --size S --datacenter FR --duration 1 #### 2. Attach and push to your instance's git repository Simple Hosting offers two "modes": the **App mode**, where an instance offers a single git repository (`default.git`) and the **Sites mode**, where you can have multiple git repositories per instance (one for each VHOST, for example `www.myapp.com.git`). Node.js, Python and Ruby instances run in App mode, whereas PHP instances run in Sites mode by default. Note: If you create a wildcard VHOST for your PHP instance, the App mode will be activated. Assuming you have local directory called `app` where you have placed your code base, you can use the following commands to create a git remote (called "gandi" by default) and push your code. $ cd app $ gandi paas attach myapp # App mode $ gandi paas attach myapp --vhost www.myapp.com # Sites mode $ git push gandi master #### 3. Deploy your code Still inside the `app` folder, you can use the following command to start the deploy process, which will checkout your code, install dependencies and launch (or relaunch) the app process: $ gandi deploy ### Creating a SSL Certificate Gandi SSL offers a range of SSL certificates to help you secure your projects. You can order, obtain, update and revoke your certificates from the command line. #### 1. Find the right plan for you $ gandi certificate plans Our Standard, Pro and Business plans offer different validation methods and guarantees. Each plan supports all or some of these types of certificates: single address, wildcard and/or multiple subdomains. To discover our offering and find the right certificate for your project, [compare our plans](https://www.gandi.net/ssl/compare) and [try our simulator](https://www.gandi.net/ssl/which-ssl-certificate). Gandi CLI can choose the right certificate type for you depending on the number of domains (altnames) you supply at creation time. You only need to set it if you plan on adding more domains to the certificate in the future. #### 2. Create the Certificate WARNING : This command is billable. To request a certificate, you need to use a private key to generate and sign a CSR (Certificate Signing Request) that will be supplied to Gandi. The `create` command will take care of this for you if you don't have them already, or you can supply your CSR directly. Check out the examples below or [our wiki](http://wiki.gandi.net/ssl) for more information on how SSL certificates work. To create a single domain Standard certificate: $ gandi certificate create --cn "domain.tld" For a wildcard Standard certificate: $ gandi certificate create --cn "*.domain.tld" For a multi domain Standard certificate: $ gandi certificate create --cn "*.domain.tld" --altnames "host1.domain.tld" --altnames "host2.domain.tld" You can also specify a plan type. For example, for a single domain Business certificate: $ gandi certificate create --cn "domain.tld" --type "bus" If you have a CSR (you can give the CSR content or the path): $ gandi certificate create --csr /path/to/csr/file #### 3. Follow the Certificate create operation $ gandi certificate follow #### 4. Get the Certificate As soon as the operation is DONE, you can export the certificate. $ gandi certificate export "domain.tld" You can also retrieve intermediate certificates if needed. $ gandi certificate export "domain.tld" --intermediate Find information on how to use your certificate with different servers on [our wiki](http://wiki.gandi.net/en/ssl). ### Adding a Web Application vhost with SSL Gandi allow you to associate a certificate with your vhost. #### 1. You already have the matching certificate at Gandi Just create the vhost giving it the private key used to generate that certificate. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" #### 2. You have the matching certificate but not at Gandi (or in another account) Declare the hosted certificate. $ gandi certstore create --pk "domain.tld.key" --crt "domain.tld.crt" And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" --ssl #### 3. You don't have any certificate and plan to get it at Gandi Create the certificate. $ gandi certificate create --cn "domain.tld.key" --type std And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" ### Creating a private VLAN You can use Gandi CLI to create and setup your private VLANs. For more detailed information on how VLANs and networking in general works at Gandi, please check out our resources: * [Creating a private VLAN with Gandi CLI](http://wiki.gandi.net/en/tutorials/cli/pvlan) * [VLAN on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network/pvlan) * [Networking on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network) #### Create a VLAN $ gandi vlan create --name my-vlan-in-lu --datacenter LU \ --subnet "192.168.1.0/24" --gateway 192.168.1.1 To create a VLAN you need to determine its `name` and `datacenter`. You can also set the `subnet` at creation time, or a default subnet will be chosen for you. The `gateway` setting is also optional and you can update both of these settings at any moment. $ gandi vlan update my-vlan-in-lu --gateway 192.168.1.254 #### Attach an existing VM to a VLAN To add an existing VM to a VLAN, you can create a private network interface and attach it to the VM. $ gandi ip create --vlan my-vlan-in-lu --attach my-existing-vm --ip 192.168.1.254 If you don't specify the IP you want to use, one will be chosen for you from the VLAN's subnet. #### Create a "Private VM" In fact there's no such thing as a "Private VM", but you can create a VM and only attach a private interface to it. $ gandi vm create --hostname my-private-vm --vlan my-vlan-in-lu --ip 192.168.1.2 Please note that a private VM cannot be accessed through the emergency console. You'll need a public VM that also has a private interface on the same VLAN to gain access. You can check out [our tutorial](http://wiki.gandi.net/en/tutorials/cli/pvlan) for an example of how to achieve this. #### More options $ gandi vlan --help Use the `--help` flag for more VLAN management options. ## Advanced Usage ### All Commands To list all available commands, type `$ gandi --help` For extended instructions, check out the `man` page. ### Build manpage Install python-docutils and run: $ rst2man --no-generator gandicli.man.rst > gandi.1.man Then to read the manpage: $ man ./gandi.1.man ### Configuration Run `$ gandi setup` to configure your settings (see [Getting started](#getting-started)) Use `$ gandi config` to set and edit custom variables. The default variables are: * `sshkey` # path to your public ssh key * `api.host` # the URL of the API endpoint to use (i.e OTE or Production) * `api.key` # the relevant API key for the chosen endpoint ## Contributing We <3 contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. You can check the [contributors list](https://github.com/Gandi/gandi.cli/graphs/contributors). ## Code status [![Build Status](https://travis-ci.org/Gandi/gandi.cli.svg?branch=master)](https://travis-ci.org/Gandi/gandi.cli) [![Coverage Status](https://coveralls.io/repos/Gandi/gandi.cli/badge.svg?branch=master)](https://coveralls.io/r/Gandi/gandi.cli?branch=master) [![Pip Version](https://img.shields.io/pypi/v/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Python Version](https://img.shields.io/pypi/pyversions/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Download Stat](https://img.shields.io/pypi/dm/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) ## License / Copying Copyright © 2014-2018 Gandi S.A.S Gandi CLI is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gandi CLI is distributed in the hope that it will be useful, but WITHOUT 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 Gandi CLI. If not, see . gandi.cli-1.2/gandi.cli.egg-info/0000755000175000017500000000000013227415174017412 5ustar sayounsayoun00000000000000gandi.cli-1.2/gandi.cli.egg-info/top_level.txt0000644000175000017500000000000613227415174022140 0ustar sayounsayoun00000000000000gandi gandi.cli-1.2/gandi.cli.egg-info/entry_points.txt0000644000175000017500000000006313227415174022707 0ustar sayounsayoun00000000000000[console_scripts] gandi = gandi.cli.__main__:main gandi.cli-1.2/gandi.cli.egg-info/namespace_packages.txt0000644000175000017500000000000613227415174023741 0ustar sayounsayoun00000000000000gandi gandi.cli-1.2/gandi.cli.egg-info/requires.txt0000644000175000017500000000011113227415174022003 0ustar sayounsayoun00000000000000setuptools pyyaml click>=3.1 requests IPy [test] nose coverage tox mock gandi.cli-1.2/gandi.cli.egg-info/not-zip-safe0000644000175000017500000000000113046337734021644 0ustar sayounsayoun00000000000000 gandi.cli-1.2/gandi.cli.egg-info/SOURCES.txt0000644000175000017500000000661013227415174021301 0ustar sayounsayoun00000000000000CHANGES.rst LICENSE MANIFEST.in README.md setup.cfg setup.py gandi/__init__.py gandi.cli.egg-info/PKG-INFO gandi.cli.egg-info/SOURCES.txt gandi.cli.egg-info/dependency_links.txt gandi.cli.egg-info/entry_points.txt gandi.cli.egg-info/namespace_packages.txt gandi.cli.egg-info/not-zip-safe gandi.cli.egg-info/requires.txt gandi.cli.egg-info/top_level.txt gandi/cli/__init__.py gandi/cli/__main__.py gandi/cli/commands/__init__.py gandi/cli/commands/account.py gandi/cli/commands/certificate.py gandi/cli/commands/certstore.py gandi/cli/commands/config.py gandi/cli/commands/contact.py gandi/cli/commands/disk.py gandi/cli/commands/dns.py gandi/cli/commands/docker.py gandi/cli/commands/domain.py gandi/cli/commands/forward.py gandi/cli/commands/ip.py gandi/cli/commands/mail.py gandi/cli/commands/oper.py gandi/cli/commands/paas.py gandi/cli/commands/record.py gandi/cli/commands/root.py gandi/cli/commands/snapshotprofile.py gandi/cli/commands/sshkey.py gandi/cli/commands/vhost.py gandi/cli/commands/vlan.py gandi/cli/commands/vm.py gandi/cli/commands/webacc.py gandi/cli/core/__init__.py gandi/cli/core/base.py gandi/cli/core/cli.py gandi/cli/core/client.py gandi/cli/core/conf.py gandi/cli/core/params.py gandi/cli/core/utils/__init__.py gandi/cli/core/utils/ascii_sparks.py gandi/cli/core/utils/password.py gandi/cli/core/utils/size.py gandi/cli/core/utils/unixpipe.py gandi/cli/core/utils/xmlrpc.py gandi/cli/modules/__init__.py gandi/cli/modules/account.py gandi/cli/modules/api.py gandi/cli/modules/cert.py gandi/cli/modules/contact.py gandi/cli/modules/datacenter.py gandi/cli/modules/disk.py gandi/cli/modules/dns.py gandi/cli/modules/docker.py gandi/cli/modules/domain.py gandi/cli/modules/forward.py gandi/cli/modules/hostedcert.py gandi/cli/modules/iaas.py gandi/cli/modules/mail.py gandi/cli/modules/metric.py gandi/cli/modules/network.py gandi/cli/modules/oper.py gandi/cli/modules/paas.py gandi/cli/modules/record.py gandi/cli/modules/snapshotprofile.py gandi/cli/modules/sshkey.py gandi/cli/modules/status.py gandi/cli/modules/vhost.py gandi/cli/modules/webacc.py gandi/cli/tests/__init__.py gandi/cli/tests/compat.py gandi/cli/tests/test_main.py gandi/cli/tests/commands/__init__.py gandi/cli/tests/commands/base.py gandi/cli/tests/commands/test_account.py gandi/cli/tests/commands/test_certificate.py gandi/cli/tests/commands/test_certstore.py gandi/cli/tests/commands/test_config.py gandi/cli/tests/commands/test_contact.py gandi/cli/tests/commands/test_disk.py gandi/cli/tests/commands/test_dns.py gandi/cli/tests/commands/test_domain.py gandi/cli/tests/commands/test_forward.py gandi/cli/tests/commands/test_ip.py gandi/cli/tests/commands/test_mail.py gandi/cli/tests/commands/test_oper.py gandi/cli/tests/commands/test_paas.py gandi/cli/tests/commands/test_record.py gandi/cli/tests/commands/test_root.py gandi/cli/tests/commands/test_snapshotprofile.py gandi/cli/tests/commands/test_sshkey.py gandi/cli/tests/commands/test_status.py gandi/cli/tests/commands/test_vhost.py gandi/cli/tests/commands/test_vlan.py gandi/cli/tests/commands/test_vm.py gandi/cli/tests/commands/test_webacc.py gandi/cli/tests/fixtures/__init__.py gandi/cli/tests/fixtures/_cert.py gandi/cli/tests/fixtures/_contact.py gandi/cli/tests/fixtures/_domain.py gandi/cli/tests/fixtures/_hosting.py gandi/cli/tests/fixtures/_operation.py gandi/cli/tests/fixtures/_paas.py gandi/cli/tests/fixtures/_version.py gandi/cli/tests/fixtures/api.py gandi/cli/tests/fixtures/mocks.pygandi.cli-1.2/gandi.cli.egg-info/dependency_links.txt0000644000175000017500000000000113227415174023460 0ustar sayounsayoun00000000000000 gandi.cli-1.2/gandi.cli.egg-info/PKG-INFO0000644000175000017500000012274513227415174020522 0ustar sayounsayoun00000000000000Metadata-Version: 1.1 Name: gandi.cli Version: 1.2 Summary: Gandi command line interface Home-page: https://github.com/Gandi/gandi.cli Author: Gandi Author-email: feedback@gandi.net License: UNKNOWN Description: # Gandi CLI Use `$ gandi` to easily create and manage web resources from the command line. * `$ gandi domain` to buy and manage your domain names * `$ gandi paas` to create and deploy your web applications * `$ gandi vm` to spin up and upgrade your virtual machines * `$ gandi certificate` to manage your ssl certificates * `$ gandi` to list all available commands * [Detailed examples](#use-cases) * [All commands](#all-commands) ## Table of contents * [Requirements](#requirements) * [Installation](#installation) * [Getting started](#getting-started) * [Use cases](#use-cases) * [Registering a Domain Name](#registering-a-domain-name) * [Creating a Virtual Machine](#creating-a-virtual-machine) * [Deploying a Web Application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) * [Advanced Usage](#advanced-usage) * [All Commands](#all-commands) * [Build manpage](#build-manpage) * [Configuration](#configuration) * [Contributing](#contributing) * [Code status](#code-status) * [License](#license) ## Requirements * A compatible operating system (Linux, BSD, Mac OS X/Darwin, Windows) * Python 2.6/2.7/3.2/3.3/3.4/3.5/3.6 * openssl * openssh * git Recommended tools * [pip](https://pip.pypa.io/en/latest/installing.html) * [virtualenv](https://virtualenv.pypa.io/en/latest/installation.html) * docker ## Installation ### Install with pip and virtualenv $ virtualenv /some/directory/gandi.cli $ source /some/directory/gandi.cli/bin/activate $ pip install gandi.cli ### Build from source $ cd /path/to/the/repository $ python setup.py install --user ### From the Debian package $ ln -sf packages/debian debian && debuild -us -uc -b && echo "Bisou" ## Getting started Using our classic (V4) website: 1. To get started, you can create a [free Gandi account](https://v4.gandi.net/contact/create) and get your Gandi Handle 2. [Generate your Production API Token](https://v4.gandi.net/admin/api_key) from the account admin section 3. You may also want to [top-up your prepaid account](https://v4.gandi.net/prepaid) 4. To manipulate VM's, you also need to [purchase credits](https://www.gandi.net/credit/buy) (you can use funds from your prepaid account) Using our latest (V5) website: 1. To get started, you can create a [free Gandi account](https://account.gandi.net/en/create_account) and get your Gandi username 2. [Generate your Production API Token](https://account.gandi.net/en/) from within the account Security section 3. You may also want to [top-up your prepaid account](https://admin.gandi.net/billing/) 4. To manipulate VM's, you currently need to follow above steps to create an account on our classic (V4) website. Then run the setup $ gandi setup > API Key: x134z5x4c5c # copy-paste your api key > Environment [production] : # press enter for Production, the default > SSH key [~/.ssh/id_rsa.pub] : # your SSH public key for hosting instances and servers See the [Advanced Usage](#advanced-usage) section for more details on configuration. ## Use cases * [Registering a domain name](#registering-a-domain-name) * [Creating a virtual machine](#creating-a-virtual-machine) * [Deploying a web application with Simple Hosting](#deploying-a-web-application-with-simple-hosting) * [Creating a SSL Certificate](#creating-a-ssl-certificate) * [Adding a Web Application vhost with SSL](#adding-a-web-application-vhost-with-ssl) * [Creating a Private VLAN](#creating-a-private-vlan) ### Registering a Domain Name Gandi is a domain name registrar since 1999. The oldest in France and one of the world's leading, Gandi is recognized for its No Bullshit™ trademark and approach to domain names. You can now buy and manage domains in any of the 500+ TLD's that Gandi offers from the command line. [Know more about Gandi Domains on the website](https://www.gandi.net/domain). #### 1. Buy a domain using the interactive prompt $ gandi domain create > Domain: example.com # enter the domain name here > example.com is available > Duration [1] : 1 # enter the duration in years This will create a domain and use your default information for Ownership, Admin, Technical and Billing info. #### 2. Buy a domain in one line $ gandi domain create --domain example.com --duration 1 #### 3. Buy a domain with custom contacts $ gandi domain create --domain example.com --duration 1 --owner XYZ123-GANDI --admin XYZ123-GANDI --tech XYZ123-GANDI --bill XYZ123-GANDI You can use the information of Gandi handles associated to Contacts in your account to setup Owner, Admin, Technical and Billing info. #### 3. List your domains $ gandi domain list #### 4. Get information about a domain $ gandi domain info example.com #### 5. Manage NS records for your domains ##### Create a new record $ gandi record create example.com --name www --type A --value 127.0.0.1 Add a new record to the domain's current zone file and activate it. ##### List your records $ gandi record list example.com List a domain's zone file records. You can use the `--format` parameter to change the output format to `text` or `json`. ##### Update one record $ gandi record update example.com --record "@ 3600 IN A 127.0.0.1" --new-record "@ 3600 IN A 0.0.0.0" This command is useful to update only one record at the time. The pattern to use is `name TTL CLASS TYPE value`. You can easily check or copy-paste the values you need to replace using the `--format text` parameter: $ gandi record list example.com --format text ##### Update many records $ gandi record list example.com --format text > file.zone Use this command to extract your zone records into a file called `file.zone` (or something else). Simply edit the file to your liking and then update the entire zone file with it. $ gandi record update example.com -f file.zone ##### Delete records $ gandi record delete example.com --value 127.0.0.1 Delete all records that match the given parameters from a domain's zone file. In this example, if there were many records with '127.0.0.1' as their value, all of them would be deleted. ### Creating a Virtual Machine Gandi Server offers powerful Xen- and Linux-based virtual machines since 2007. Virtual machines can be configured and upgraded on the fly to your liking. For example, you can start with 1GB of RAM, and run a command to add 2GB of RAM and 2 CPUs without even having to restart it. Gandi Server measures consumption by the hour and uses a prepaid credit system. To learn more, [check out the Gandi Server website](https://www.gandi.net/hosting/server/). #### 1. Create and access a VM $ gandi vm create * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 1 cores, 256Mb memory, ip v4+v6, image Debian 8, hostname: temp1415183684, datacenter: LU Create a virtual machine with the default configuration and a random hostname. #### 2. Upgrade a VM $ gandi vm update temp1415183684 --memory 2048 --cores 2 Set the VM's RAM to 2GB and add a CPU core on the fly. #### 3. Create a custom VM $ gandi vm create --datacenter US --hostname docker --cores 2 --memory 3072 --size 10240 --image "Ubuntu 14.04 64 bits LTS (HVM)" --run "curl -sSL https://get.docker.com/ubuntu/ | sh" * root user will be created. * SSH key authorization will be used. * No password supplied for vm (required to enable emergency web console access). * Configuration used: 2 cores, 3072Mb memory, ip v4+v6, image Ubuntu 14.04 64 bits LTS, hostname: docker, datacenter: LU This command will setup the above VM, and install docker by running `curl -sSL https://get.docker.com/ubuntu/ | sh` after creation. #### 4. View your resources $ gandi vm list #### 5. Get all the details about a VM $ gandi vm info docker ### Deploying a Web Application with Simple Hosting Gandi Simple Hosting is a PaaS (Platform as a Service) offering fast code deployment and easy scaling, powering over 50,000 apps since its inception in 2012. Instances can run apps in 4 languages (PHP, Python, Node.js and Ruby) along with one of 3 popular databases (MySQL, PostgreSQL and MongoDB) and operate on a managed platform with built-in http caching. Plans cover all scales, from small to world-class projects. [Check out the website for more information](https://www.gandi.net/hosting/simple). #### 1. Create a Simple Hosting instance $ gandi paas create --name myapp --type nodejspgsql --size S --datacenter FR --duration 1 #### 2. Attach and push to your instance's git repository Simple Hosting offers two "modes": the **App mode**, where an instance offers a single git repository (`default.git`) and the **Sites mode**, where you can have multiple git repositories per instance (one for each VHOST, for example `www.myapp.com.git`). Node.js, Python and Ruby instances run in App mode, whereas PHP instances run in Sites mode by default. Note: If you create a wildcard VHOST for your PHP instance, the App mode will be activated. Assuming you have local directory called `app` where you have placed your code base, you can use the following commands to create a git remote (called "gandi" by default) and push your code. $ cd app $ gandi paas attach myapp # App mode $ gandi paas attach myapp --vhost www.myapp.com # Sites mode $ git push gandi master #### 3. Deploy your code Still inside the `app` folder, you can use the following command to start the deploy process, which will checkout your code, install dependencies and launch (or relaunch) the app process: $ gandi deploy ### Creating a SSL Certificate Gandi SSL offers a range of SSL certificates to help you secure your projects. You can order, obtain, update and revoke your certificates from the command line. #### 1. Find the right plan for you $ gandi certificate plans Our Standard, Pro and Business plans offer different validation methods and guarantees. Each plan supports all or some of these types of certificates: single address, wildcard and/or multiple subdomains. To discover our offering and find the right certificate for your project, [compare our plans](https://www.gandi.net/ssl/compare) and [try our simulator](https://www.gandi.net/ssl/which-ssl-certificate). Gandi CLI can choose the right certificate type for you depending on the number of domains (altnames) you supply at creation time. You only need to set it if you plan on adding more domains to the certificate in the future. #### 2. Create the Certificate WARNING : This command is billable. To request a certificate, you need to use a private key to generate and sign a CSR (Certificate Signing Request) that will be supplied to Gandi. The `create` command will take care of this for you if you don't have them already, or you can supply your CSR directly. Check out the examples below or [our wiki](http://wiki.gandi.net/ssl) for more information on how SSL certificates work. To create a single domain Standard certificate: $ gandi certificate create --cn "domain.tld" For a wildcard Standard certificate: $ gandi certificate create --cn "*.domain.tld" For a multi domain Standard certificate: $ gandi certificate create --cn "*.domain.tld" --altnames "host1.domain.tld" --altnames "host2.domain.tld" You can also specify a plan type. For example, for a single domain Business certificate: $ gandi certificate create --cn "domain.tld" --type "bus" If you have a CSR (you can give the CSR content or the path): $ gandi certificate create --csr /path/to/csr/file #### 3. Follow the Certificate create operation $ gandi certificate follow #### 4. Get the Certificate As soon as the operation is DONE, you can export the certificate. $ gandi certificate export "domain.tld" You can also retrieve intermediate certificates if needed. $ gandi certificate export "domain.tld" --intermediate Find information on how to use your certificate with different servers on [our wiki](http://wiki.gandi.net/en/ssl). ### Adding a Web Application vhost with SSL Gandi allow you to associate a certificate with your vhost. #### 1. You already have the matching certificate at Gandi Just create the vhost giving it the private key used to generate that certificate. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" #### 2. You have the matching certificate but not at Gandi (or in another account) Declare the hosted certificate. $ gandi certstore create --pk "domain.tld.key" --crt "domain.tld.crt" And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" --ssl #### 3. You don't have any certificate and plan to get it at Gandi Create the certificate. $ gandi certificate create --cn "domain.tld.key" --type std And then create the vhost. $ gandi vhost create domain.tld --paas "PaasName" \ --ssl --private-key "domain.tld.key" ### Creating a private VLAN You can use Gandi CLI to create and setup your private VLANs. For more detailed information on how VLANs and networking in general works at Gandi, please check out our resources: * [Creating a private VLAN with Gandi CLI](http://wiki.gandi.net/en/tutorials/cli/pvlan) * [VLAN on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network/pvlan) * [Networking on Gandi Wiki](http://wiki.gandi.net/en/iaas/references/network) #### Create a VLAN $ gandi vlan create --name my-vlan-in-lu --datacenter LU \ --subnet "192.168.1.0/24" --gateway 192.168.1.1 To create a VLAN you need to determine its `name` and `datacenter`. You can also set the `subnet` at creation time, or a default subnet will be chosen for you. The `gateway` setting is also optional and you can update both of these settings at any moment. $ gandi vlan update my-vlan-in-lu --gateway 192.168.1.254 #### Attach an existing VM to a VLAN To add an existing VM to a VLAN, you can create a private network interface and attach it to the VM. $ gandi ip create --vlan my-vlan-in-lu --attach my-existing-vm --ip 192.168.1.254 If you don't specify the IP you want to use, one will be chosen for you from the VLAN's subnet. #### Create a "Private VM" In fact there's no such thing as a "Private VM", but you can create a VM and only attach a private interface to it. $ gandi vm create --hostname my-private-vm --vlan my-vlan-in-lu --ip 192.168.1.2 Please note that a private VM cannot be accessed through the emergency console. You'll need a public VM that also has a private interface on the same VLAN to gain access. You can check out [our tutorial](http://wiki.gandi.net/en/tutorials/cli/pvlan) for an example of how to achieve this. #### More options $ gandi vlan --help Use the `--help` flag for more VLAN management options. ## Advanced Usage ### All Commands To list all available commands, type `$ gandi --help` For extended instructions, check out the `man` page. ### Build manpage Install python-docutils and run: $ rst2man --no-generator gandicli.man.rst > gandi.1.man Then to read the manpage: $ man ./gandi.1.man ### Configuration Run `$ gandi setup` to configure your settings (see [Getting started](#getting-started)) Use `$ gandi config` to set and edit custom variables. The default variables are: * `sshkey` # path to your public ssh key * `api.host` # the URL of the API endpoint to use (i.e OTE or Production) * `api.key` # the relevant API key for the chosen endpoint ## Contributing We <3 contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. You can check the [contributors list](https://github.com/Gandi/gandi.cli/graphs/contributors). ## Code status [![Build Status](https://travis-ci.org/Gandi/gandi.cli.svg?branch=master)](https://travis-ci.org/Gandi/gandi.cli) [![Coverage Status](https://coveralls.io/repos/Gandi/gandi.cli/badge.svg?branch=master)](https://coveralls.io/r/Gandi/gandi.cli?branch=master) [![Pip Version](https://img.shields.io/pypi/v/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Python Version](https://img.shields.io/pypi/pyversions/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) [![Download Stat](https://img.shields.io/pypi/dm/gandi.cli.svg)](https://pypi.python.org/pypi/gandi.cli) ## License / Copying Copyright © 2014-2018 Gandi S.A.S Gandi CLI is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gandi CLI is distributed in the hope that it will be useful, but WITHOUT 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 Gandi CLI. If not, see . Changelog ========= 1.2 --- * Add support for paas size s+ for creation/update * Fixes #232: Update 'gandi record update' command to allow filtering by name * Fix bug when attempting to migrate a vm which cannot be migrated * Only display DC closed warning if a date is set 1.1 --- * FR-SD5 is now the default datacenter. * Add new 'gandi dns update' command. * Fixes #228: Generate a user password at the creation of a VM * Improve wait for ssh connectivity after 'gandi vm create' command to handle ipv6 * Fix a bug with 'gandi disk migrate' command not working with multiple datacenters choices * Improve documentation for generating username/apikey with Gandi V5 1.0 ---- * New 'dns' namespace to manage DNS records/dnssec through LiveDNS API. * Add new 'gandi vm migrate' command. * Refactor internal click code usage. Remove hackish code to handle nested commands which was limited to only 1 nested level. - This change will break code of users which were using custom commands on top of Gandi CLI, To fix this you have to use the proper click syntax to declare a new group for your commands. - This change also remove the automatic listing of all namespace commands upon a typo or unknown/wrong command. * Fixes #224: DeprecationWarning makes tests fail with python 3.6.2 0.22 ---- * Fixes #223: 'gandi setup' command error * Fixes #222: AttributeError during vm creation on a private vlan * Fixes tests for 'gandi deploy' and 'gandi status' commands 0.21 ---- * Add new 'gandi disk migrate' command * Update 'gandi setup' command to ask for apikey for REST API * Handle deprecated images - Add a warning during 'gandi vm create' command - Display a * before image labels on 'gandi vm create' help - Display a /!\ DEPRECATED on 'gandi vm images' command * Fixes #220: gandi record update issues - Do not cast to int the id of the record, use the retrieve value - Handle both record syntax with 'IN' or not when parsing - Delete created zone if record.update call fail from xmlrpc API * Fixes #219: Can't remove disk snapshot profile * vm: delete: Fix delete when we reach the list limit - Fixed a bug when deleting a vm that wasn't listed in the first 500 results of gandi.iaas.list. * Fix issue when updating disk kernel with a kernel from another datacenter - CLI was proposing only kernels available on datacenter 1, but some kernels are available only on other datacenters, so we list everything for --kernel parameters, and for disk update command we add a new check if this kernel is available for this disk on this datacenter. * Add epilog to help messages to notify user about man documentation * Add one new verbose level for dumping data 0.20 ---- * Add support for python3.6 * Debian 8 is the new default VM image * FR-SD3 is the new default datacenter * Update 'gandi mail create' command to allow passing password as parameter * Update 'gandi certificate create' command: duration is now limited to 2 years * Update 'gandi ip create' command to fix bad units in help message * Fixes #182: 'gandi disk create' will detect datacenter when creating a new VM disk * Fixes #184: 'gandi disk list' can now filter for attach/detach state * Fixes #192: 'gandi certificate info' now still works after 500 certificates * Fixes #201: 'gandi certificate export' was duplicating intermediate certificate * Fixes #211: 'gandi paas deploy' tests should work again when using git commands * Fixes a bug with options not using corrected value when deprecated * Update unixpipe module to remove usage of posix and non portable imports 0.19 ---- * Update create commands for namespaces: vm, paas, ip, disk, vlan, webacc to handle new datacenter status: - prevent using a closed datacenter for creation - display a warning when using a datacenter which will be closed in the future * Update 'gandi mailbox info' command: aliases are now sorted * Fixes #178: 'gandi account info' command now display prepaid amount * Fixes #185: 'gandi domain create' command can now change nameservers * Fixes #187: 'gandi record list' command has a --limit parameter * Fixes #188: broken links in README * Fixes certificate unittest for python3 0.18 ---- * Update 'gandi paas update' command: --upgrade parameter is now a boolean flag * Update 'gandi deploy' command: - new '--remote' and '--branch' options - better handling of case when git configuration is not configured as expected - will try and use the gandi remote by default to extract deploy url - will deploy the branch master by default - will fallback to guessing the Simple Hosting remote from git configuration of the branch to deploy - improve error message when unable to execute * Update VM spin up timeout to 5min (from 2min) for bigger VM. * Add more unittests. 0.17 ---- * Gandi CLI now supports python3.5 * Update 'gandi paas' namespace: - Add new command 'gandi paas attach' to add an instance vhost's git remote to local git repository. - Update 'gandi deploy' command: - don't need a local configuration file anymore - need to be called on attached paas instance - Update 'gandi paas clone' command: - you can now specify which vhost and local directory to use - Use correct prefix for name generation in create command * Convert 'gandi config' command to a namespace to allow configuration display and edition * Fixes bug with 'gandi account' command which was broken sometimes * Fixes a bug with 'gandi vlan update' command when using --create flag * Fixes a bug with mail alias update when using same number of alias add/del parameters. * Fixes a bug when using a resource name and having more than 100 items of this resource type * Fixes size parameter choices for 'gandi paas create' command. * Fixes bug with 'gandi record update' command and argument parsing * Fixes bug with 'gandi record' commands: - must always exit if wrong/missing input parameter. * Always display CLI full help message when requesting an unknown command * Be less aggressive when trying to connect via SSH during 'gandi vm create' * Better handling of no hosting credits error. * Add more unittests. * Fixes #108 * Fixes #128 * Fixes #140 * Fixes #157 * Fixes #161 * Fixes #165 * Fixes #170 * Fixes #173 0.16 ---- * Update parameter '--datacenter': - allow dc_code as optional value - old values: FR/LU/US are still working so it doesn't break compatibility but they will be deprecated in next releases * Update output of IP creation to display IP address: - for 'gandi ip create' command - for 'gandi vm create' command with --ip option * Various improvements to modules for library usage: - datacenter - account - domain - operations * Update 'gandi mail info' command: - change output of responder and quota information to be more user friendly * Update click requirement version to >= 3.1 so we always use the latest version * Fixes debian python3 packaging * Fixes #148 * Fixes #147 0.15 ---- * New command 'gandi domain renew' command to renew a domain. * Update 'domain info' command: - add creation, update and expiration date to output - changes nameservers and services output for easier parsing * Update 'gandi domain create' command: - the domain name can now be passed as argument, the option --domain will be deprecated upon next release. * Update 'gandi disk update' command: - add new option '--delete-snapshotprofile' to remove a snapshot profile from disk * Update 'gandi ip delete' command: - now accept multiple IP as argument in order to delete a list of IP addresses * Fixes #119 * Fixes #129 * Fixes #141 0.14 ---- * New 'certstore' namespace to manage certificates in webaccs. * New command 'gandi vhost update' to activate ssl on the vhost. * Update 'gandi vhost create' and 'gandi vhost update' commands to handle hosted certificates. * Update 'gandi paas create' command to handle hosted certificates. * Update 'gandi webacc create' and add to handle hosted certificates. * Update 'gandi paas info' command: - add new --stat parameter, which will display cached page statistic based on the last 24 hours. - add snapshotprofile information to output. * Update 'gandi oper list' command to add filter on step type. * Update 'gandi paas update' command to allow deleting an existing snapshotprofile. * Update 'gandi status' command to also display current incidents not attached to a specific service. * Fixes #132 * Fixes #131 * Fixes #130 * Fixes #120 * Fixes error message when API is not reachable. 0.13 ---- * New 'webacc' namespace for managing web accelerators for virtual machines. * New command 'gandi status' to display Gandi services statuses. * New command 'gandi ip update' to update reverse (PTR record) * Update 'gandi vm create' command to add new parameter --ssh to open a SSH session to the machine after creation is complete. This means that the previous behavior is changed and vm creation will not automatically open a session anymore. * Update several commands with statistics information: - add disk quota usage in 'gandi paas info' command - add disk network and vm network stats in 'gandi vm info' command * Update 'gandi account info' command to display credit usage per hour * Update 'gandi certificate update' command to displays how to follow and retrieve the certificate after completing the process. * Update 'gandi ip info' command to display reverse information * Update 'gandi ip list' command to add vlan filtering * Update 'gandi vm list' command to add datacenter filtering * Update 'gandi vm create' command to allow usage of a size suffix for --size parameter (as in disk commands) * Update 'gandi vm ssh' command to add new parameter --wait to wait for * Update 'certificate' namespace: - 'gandi certificate follow' command to know in which step of the process is the current operation - 'gandi certificate packages' display has been enhanced - 'gandi certificate create' will try to guess the number of altnames or wildcard - 'gandi certificate export' will retrieve the correct intermediate certificate. * Update 'gandi disk attach' command to enable mounting in read-only and also specify position where disk should be attached. * Update 'gandi record list' command with new parameter --format * Update 'gandi record update' command to update only one record in the zone file * Update 'gandi vm list' command to add datacenter filtering * Refactor code for 'gandi ip attach' and 'gandi ip delete' commands virtual machine sshd to come up (timeout 2min). * Refactor 'gandi vm create' command to pass the script directly to the API and not use scp manually after creation. * Fixes wording and various typos in documentation and help pages. * Add more unittests. * Add tox and httpretty to tests packages requirements for unittests 0.12 ---- * New 'ip' namespace with commands for managing public/private ip resources. * New 'vlan' namespace with commands for managing vlans for virtual machines. * New command 'gandi account info' to display information about credits amount for hosting account. * New command 'gandi contact create' to create a new contact. * New command 'gandi disk snapshot' to create a disk snapshot on the fly. * Update 'gandi vm create' command: - enabling creation of vlan and ip assignment for this vlan directly during vm creation. - enabling creation of a private only ip virtual machine. - parameter --ip-version is not read from configuration file anymore, still defaulting to 4. * Update 'gandi paas create' command to allow again the use of password provided on the command line. * Update 'record' namespace to add delete/update commands, with option to export zones to file. * Use different prefix for temporary names based on type of resource. * Switch to use HVM image as default disk image when creating virtual machine. * Add kernel information to output of 'gandi disk list' command. * Fixes bug with paas vhost directory creation. * Fixes bug with 'gandi mail delete' command raising a traceback. * Fixes bug with duplicates entries in commands accepting multiple resources. * Fixes various typos in documentation and help pages. * Add first batch of unittests. 0.11 ---- * New command 'gandi disk detach' to detach disks from currently attached vm. * New command 'gandi disk attach' to attach disk to a vm. * New command 'gandi disk rollback' to perform a rollback from a snapshot. * New parameter --source for command 'gandi disk create' to allow creation of a new disk from an existing disk or snapshot. * New parameter --script for command 'gandi vm create' to allow upload of a local script on freshly created vm to be run after creation is completed. * Update parameter --size of 'gandi disk create/update' command to accept optionnal suffix: M,G,T (from megabytes up to terabytes). * Update command 'gandi vm ssh' to accept args to be passed to launched ssh command. * Fixes bug with 'gandi vm create' command and image parameter, which failed when having more than 100 disks in account. * Fixes bug with 'gandi paas info' command to display sftp_server url. * Fixes bug with 'gandi record list' command when requesting a domain not managed at Gandi. * Rename --sshkey parameter of 'gandi sshkey create' command to --filename. * Prettify output of list/info commands. * GANDI_CONFIG environment variable can be used to override the global configuration file. * Bump click requirement version to <= 4. 0.10 ---- * Add new dependency to request library, for certificate validation during xmlrpc calls. * New command 'gandi vm kernels' to list available kernels, can also be used to filter by vm to know which kernel is compatible. * New parameters --cmdline and --kernels for command 'gandi disk update' to enable updating of cmdline and/or kernel. * New parameter --size for command 'gandi vm create' to specify disk size during vm creation. * Handle max_memory setting in command 'gandi vm update' when updating memory. New parameter --reboot added to accept a VM reboot for non-live update. * Update command 'gandi vm images' to also display usable disks as image for vm creation. * Security: validate server certificate using request as xmlrpc transport. * Security: restrict configuration file rights to owner only. * Refactor code of custom parameters, to only query API when needed, improving overall speed of all commands. * Fixes bug with sshkey parameter for 'gandi paas create' and 'gandi paas update' commands. * When an API call fail, we can call again using dry-run flag to get more explicit errors. Used by 'gandi vhost create' command. * Allow Gandi CLI to load custom modules using 'GANDICLI_PATH' environment variable, was previously only done by commands. 0.9 --- * New command 'gandi docker' to manage docker instance. This requires a docker client to work. * Improve 'vm ssh' command to support identity file, login@ syntax. * Login is no longer a mandatory option and saved to configuration when creating a virtual machine. * Add short summary to output when creating a virtual machine. * Fixes bug when no sshkey available during setup. * Fixes bug with parameters validation when calling a command before having entered api credentials. 0.8 --- * New record namespace to manage domain zone record entries 0.7 --- * Add and update License information to use GPL-3 * Uniformize help strings during creation/deletion commands 0.6 --- * New mail namespace for managing mailboxes and aliases * New command 'disk create' to create a virtual disk * New command 'vm ssh' to open a ssh connection to an existing virtual machine * New command 'help' which behave like --help option. * Using 'gandi namespace' without full command will display list of available commands for this namespace and associated short help. * 'gandi paas create' and 'gandi vm create' commands now use sshkeys, and default to LU as default datacenter. 0.5 --- * Fixes Debian packaging 0.4 --- * Fixes bug with snapshotprofile list command preventing 'gandi setup' to work after clean installation * Allow Gandi CLI to load custom modules/commands using 'GANDICLI_PATH' environment variable 0.3 --- * New certificate namespace for managing certificates * New disk namespace for managing iaas disks * New snapshotprofile namespace to know which profiles exists * Allow override of configuration values for apikey, apienv and apihost using shell environment variables API_KEY, API_ENV, API_HOST. * Bugfixes on various vm and paas commands * Fixes typos in docstrings * Update man page 0.2 --- * New vhost namespace for managing virtual host for PaaS instances * New sshkey namespace for managing a sshkey keyring * Bugfixes on various vm and paas commands * Bugfixes when using a hostname using only numbers * Added a random unique name generated for temporary VM and PaaS 0.1 --- * Initial release Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Terminals Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)