Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc96bd21f | |||
| 66fac2e82f | |||
| 39c80262d4 | |||
| b5dfad2c38 | |||
| bc9a09e3fc | |||
| 21ae9a02d6 | |||
| 39aea8c5ee | |||
| 82bbbbbf32 | |||
| 358e71d98e | |||
| 29af228a96 | |||
| e8401a08a8 | |||
| e994ff605d | |||
| 8dc8fcce77 | |||
| 3a9b00fa4e | |||
| 8d84d0cafa | |||
| 244a0cc569 | |||
| a5c2d6014f | |||
| 13bff7e353 | |||
| 9a318ceddc | |||
| 213cfb3dad | |||
| 73a2e52a75 | |||
| b308c942d5 | |||
| 6d87e6aa76 | |||
| 5cd45b1278 | |||
| c5790de3db | |||
| 8d638aaa75 | |||
| 0836c46e64 | |||
| da47d46c45 | |||
| bd9c54e820 | |||
| 0d0c403037 | |||
| 09fabf1e22 | |||
| 7720be6d84 | |||
| 2291d5ba79 | |||
| ef1378462c | |||
| 9c37cf8ef1 | |||
| 8351d01c71 | |||
| 32a5d3c2b7 | |||
| 3e4b19a1e5 | |||
| cf62c812f3 | |||
| 90b97b147b | |||
| 2ba65c2328 | |||
| 7884eb5bdf | |||
| b229e02d33 | |||
| 014fce77d8 | |||
| b4fe0530d8 | |||
| 21025f64d5 | |||
| 7b32212c97 | |||
| 95767b9d85 | |||
| 47c7a30bbd | |||
| 962e5b1888 | |||
| 2e0a051539 | |||
| d16d5eeec5 | |||
| 9e786cf754 | |||
| 227ab55617 | |||
| 0f0041725e | |||
| accc2c27f5 | |||
| 32e319920c | |||
| dd92d492ee | |||
| 3e1d902560 | |||
| e3aada4a2f | |||
| 504ba96506 | |||
| 11a4604102 | |||
| f6ae2f242c | |||
| 3cf1a2fbf4 | |||
| a33fac607c | |||
| 14551e4af7 | |||
| b117f617cf | |||
| b9dbefb21a | |||
| 2f77d06756 | |||
| 7f68cece3a | |||
| d09debd85a | |||
| 3bff2e5ffd | |||
| c13d867b95 | |||
| e38b84a2f3 | |||
| 5772937515 | |||
| d7ec646506 | |||
| f0255837d4 | |||
| 7c2475e886 | |||
| b06b8e67f5 | |||
| 3fb6fa2444 | |||
| e1cca94579 | |||
| 1b64c62faf | |||
| 777c869c61 | |||
| 972a28c4f4 | |||
| 3259bd7e23 | |||
| 9e8b1b424b | |||
| 58e677d112 | |||
| 4fb31f1cab | |||
| c51a233970 | |||
| fc5c63d697 | |||
| d4cdccb51b | |||
| f6113de170 | |||
| f68639c5fa | |||
| 688be43811 | |||
| 1e6fd243b1 | |||
| d1ade20413 | |||
| 436d796689 | |||
| 2ac81bfc4d | |||
| fb139b2925 | |||
| 8f7ea692c6 | |||
| cf2fb01edb | |||
| 4d41d90c97 | |||
| fc9604abb0 | |||
| 641c626f11 | |||
| e082abea28 | |||
| 7411e8dc41 | |||
| d809444065 | |||
| 456aec3390 | |||
| c29c4ff968 | |||
| 62406b5ab5 | |||
| 0c59ad8087 | |||
| 702cef84fa | |||
| 892299346f | |||
| 7c98be0e52 | |||
| 1ced19c3ce | |||
| 1ecfe4b274 | |||
| b87cdda9e4 | |||
| 577e6caa1a | |||
| 84d84199f6 | |||
| bbb8dd774a | |||
| b2b85d460f | |||
| 6538163094 | |||
| c2d18a28a4 | |||
| 170198157b | |||
| 79eb0cdfb1 | |||
| 684b6b182d | |||
| 7eaccfd522 | |||
| 838aadc579 | |||
| 5d5ec90592 | |||
| 3721ba72d8 | |||
| 7b1777cf37 | |||
| 089f00b6f1 | |||
| dcf34b6578 | |||
| 40a9356b9f | |||
| 0ac56cc13d | |||
| 0d6dc0852c | |||
| 5448a997cc | |||
| c0981c412e | |||
| 2d1707ec2a | |||
| 0b6d0efa7b | |||
| bf664fce1b | |||
| 55889aee64 | |||
| e9593bf49e | |||
| a68f6bf6de | |||
| b019a15c80 | |||
| 581173eeb8 | |||
| a148672433 | |||
| 7efbfa0b15 | |||
| c573cf2370 | |||
| 04a2afd8ee | |||
| 0e421d2eb0 | |||
| badad141d3 | |||
| ebccdda8ab | |||
| 091533246e | |||
| 95da55c0fa | |||
| 23712375aa | |||
| 7586b6ec90 | |||
| 0aa9b2df78 | |||
| 525813722d | |||
| 6e805be1d6 | |||
| 6bb15b7d58 | |||
| 79f230b0b0 | |||
| 641ece6376 | |||
| b37c171676 | |||
| d1c69048ed | |||
| 8b9263d38f | |||
| 848985b200 | |||
| 0028563253 | |||
| 7db824f457 | |||
| c490911eb6 | |||
| cac0f49bb8 | |||
| 6d57e0c530 | |||
| 0235a00545 | |||
| 0770e2ddec | |||
| 214d679e2d | |||
| 7dbe330099 | |||
| f76027d449 | |||
| 360a6939a4 | |||
| d3e120c6fb | |||
| dabe7ac584 | |||
| 221281b714 | |||
| 72f5d84cb7 | |||
| 91253c4f32 | |||
| c3de114585 | |||
| 593295c7d9 | |||
| b5546262ca | |||
| 6b42b3b329 | |||
| a8b66cf020 | |||
| 6e0685047e | |||
| 9e78cbaa89 | |||
| 44087ca7df | |||
| 78de34538d | |||
| 31982bcfc6 | |||
| 338298b8af | |||
| 0edeb49467 | |||
| 4f229dea09 | |||
| 0f354c79d1 | |||
| ce1900c477 | |||
| 1bf8237d64 | |||
| c992de45a3 | |||
| 51fe46e6d9 | |||
| 9b9af6dc9d | |||
| ca7e8dbb74 | |||
| e2534fe88d | |||
| 04a8bfb455 | |||
| 1015df8fa8 | |||
| dab3eb5ece | |||
| 91a6a7797d | |||
| 18f9677b54 | |||
| 6a549f0548 | |||
| 7522c36112 | |||
| 9bd36a76e8 | |||
| e02afe1775 | |||
| a6e0ea8bd2 | |||
| 148d95def5 | |||
| 75db279364 | |||
| e1c0616a14 | |||
| dc6519f7c1 | |||
| 206ec4e5bf | |||
| 8d41363085 | |||
| e0a4e72be5 | |||
| 1a6f410257 | |||
| 7127ad967b | |||
| 9bcca1a77b | |||
| 7d63c196d5 | |||
| d31b2d0038 | |||
| 1cbd3d6a9f | |||
| 46d3e7c1a4 | |||
| 1e6780e496 | |||
| 031b20f197 | |||
| 54e8eaab1c | |||
| 875ee0a871 | |||
| c1e7d4314f | |||
| 676041dc15 | |||
| 7f8cd0caf7 | |||
| abbf9798b4 | |||
| 3435e63b52 | |||
| 793877568f | |||
| ba1c7101c5 | |||
| d0dc0a769e | |||
| 5b31c2f073 | |||
| a6eea4642a | |||
| 8ba8932552 | |||
| a774e2b444 | |||
| dd6e4e4933 | |||
| 4e6993909c | |||
| 8599af62a4 | |||
| 787a3178b3 |
+2
-1
@@ -1,4 +1,5 @@
|
||||
/bin
|
||||
/local
|
||||
/pkg
|
||||
/website/.sass-cache
|
||||
/website/build
|
||||
packerrc
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.1.0 (June 28, 2013)
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -8,7 +8,7 @@ all:
|
||||
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
@go get -d -v ./...
|
||||
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
|
||||
@./build.sh
|
||||
@./scripts/build.sh
|
||||
|
||||
format:
|
||||
go fmt ./...
|
||||
|
||||
@@ -11,12 +11,19 @@ Packer is lightweight, runs on every major operating system, and is highly
|
||||
performant, creating machine images for multiple platforms in parallel.
|
||||
Packer comes out of the box with support for creating AMIs (EC2), VMware
|
||||
images, and VirtualBox images. Support for more platforms can be added via
|
||||
plugins.
|
||||
plugins. The images that Packer creates an easily be turned into
|
||||
[Vagrant](http://www.vagrantup.com) boxes.
|
||||
|
||||
## Quick Start
|
||||
|
||||
First, get Packer by either downloading a pre-built Packer binary for
|
||||
your operating system or [downloading and compiling Packer yourself](#developing-packer).
|
||||
**Note:** There is a great
|
||||
[introduction and getting started guide](http://localhost:4567/intro)
|
||||
for those with a bit more patience. Otherwise, the quick start below
|
||||
will get you up and running quickly, at the sacrifice of not explaining some
|
||||
key points.
|
||||
|
||||
First, [downloada pre-built Packer binary](http://localhost:4567/downloads.html)
|
||||
for your operating system or [compile Packer yourself](#developing-packer).
|
||||
|
||||
After Packer is installed, create your first template, which tells Packer
|
||||
what platforms to build images for and how you want to build them. In our
|
||||
@@ -32,9 +39,9 @@ own.
|
||||
"secret_key": "YOUR SECRET KEY HERE",
|
||||
"region": "us-east-1",
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "m1.small",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-quick-start {{.CreateTime}}"
|
||||
"ami_name": "packer-example {{.CreateTime}}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
* builder/amazonebs: Copy AMI to multiple regions
|
||||
* builder/vmware: Downloading the ISO
|
||||
* builder/vmware: VMX templates
|
||||
* communicator/ssh: Ability to re-establish connection
|
||||
* communicator/ssh: Download()
|
||||
* packer: Communicator should have Close() method
|
||||
* packer/plugin: Better error messages/detection if plugin crashes
|
||||
* packer/plugin: Testing of client struct/methods
|
||||
* provisioner/shell: Arguments
|
||||
|
||||
@@ -2,12 +2,18 @@ package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type artifact struct {
|
||||
// A map of regions to AMI IDs.
|
||||
amis map[string]string
|
||||
|
||||
// EC2 connection for performing API stuff.
|
||||
conn *ec2.EC2
|
||||
}
|
||||
|
||||
func (*artifact) BuilderId() string {
|
||||
@@ -19,9 +25,13 @@ func (*artifact) Files() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*artifact) Id() string {
|
||||
// TODO(mitchellh): Id
|
||||
return "TODO"
|
||||
func (a *artifact) Id() string {
|
||||
parts := make([]string, 0, len(a.amis))
|
||||
for region, amiId := range a.amis {
|
||||
parts = append(parts, fmt.Sprintf("%s:%s", region, amiId))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (a *artifact) String() string {
|
||||
@@ -33,3 +43,26 @@ func (a *artifact) String() string {
|
||||
|
||||
return fmt.Sprintf("AMIs were created:\n\n%s", strings.Join(amiStrings, "\n"))
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
errors := make([]error, 0)
|
||||
|
||||
for _, imageId := range a.amis {
|
||||
log.Printf("Degistering image ID: %s", imageId)
|
||||
if _, err := a.conn.DeregisterImage(imageId); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
// TODO(mitchellh): Delete the snapshots associated with an AMI too
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
if len(errors) == 1 {
|
||||
return errors[0]
|
||||
} else {
|
||||
return &packer.MultiError{errors}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,20 @@ func TestArtifact_Impl(t *testing.T) {
|
||||
assert.Implementor(&artifact{}, &actual, "should be an Artifact")
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
expected := `east:foo,west:bar`
|
||||
|
||||
amis := make(map[string]string)
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &artifact{amis, nil}
|
||||
result := a.Id()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -25,7 +39,7 @@ west: bar`
|
||||
amis["east"] = "foo"
|
||||
amis["west"] = "bar"
|
||||
|
||||
a := &artifact{amis}
|
||||
a := &artifact{amis, nil}
|
||||
result := a.String()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ type config struct {
|
||||
// Configuration of the resulting AMI
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
}
|
||||
|
||||
@@ -98,6 +99,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = append(errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &packer.MultiError{errs}
|
||||
}
|
||||
@@ -145,13 +155,23 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there are no AMIs, then jsut return
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If there are no AMIs, then just return
|
||||
if _, ok := state["amis"]; !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
return &artifact{state["amis"].(map[string]string)}, nil
|
||||
artifact := &artifact{
|
||||
amis: state["amis"].(map[string]string),
|
||||
conn: ec2conn,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
|
||||
@@ -13,6 +13,7 @@ func testConfig() map[string]interface{} {
|
||||
"instance_type": "foo",
|
||||
"region": "us-east-1",
|
||||
"ssh_username": "root",
|
||||
"ami_name": "foo",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +61,34 @@ func TestBuilderPrepare_AccessKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["ami_name"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ami_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "ami_name")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InstanceType(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -2,6 +2,7 @@ package amazonebs
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
@@ -27,7 +28,9 @@ func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction
|
||||
keyring := &ssh.SimpleKeychain{}
|
||||
err := keyring.AddPEMKey(privateKey)
|
||||
if err != nil {
|
||||
ui.Say(fmt.Sprintf("Error setting up SSH config: %s", err))
|
||||
err := fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -85,7 +88,9 @@ ConnectWaitLoop:
|
||||
// We connected. Just break the loop.
|
||||
break ConnectWaitLoop
|
||||
case <-timeout:
|
||||
ui.Error("Timeout while waiting to connect to SSH.")
|
||||
err := errors.New("Timeout waiting for SSH to become available.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
@@ -101,7 +106,9 @@ ConnectWaitLoop:
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error connecting to SSH: %s", err))
|
||||
err := fmt.Errorf("Error connecting to SSH: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating AMI: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
@@ -57,6 +59,8 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
for {
|
||||
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error querying images: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ func (s *stepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := ec2conn.CreateKeyPair(keyName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary keypair: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
log.Println("Running the provision hook")
|
||||
hook.Run(packer.HookProvision, ui, comm, nil)
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source instance: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
@@ -41,6 +43,8 @@ func (s *stepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
ui.Say("Waiting for instance to become ready...")
|
||||
s.instance, err = waitForState(ec2conn, s.instance, []string{"pending"}, "running")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance to become ready: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
@@ -61,5 +65,9 @@ func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
|
||||
ui.Say("Terminating the source AWS instance...")
|
||||
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
pending := []string{"pending", "running", "shutting-down", "stopped", "stopping"}
|
||||
waitForState(ec2conn, s.instance, pending, "terminated")
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ func (s *stepSecurityGroup) Run(state map[string]interface{}) multistep.StepActi
|
||||
|
||||
ui.Say("Authorizing SSH access on the temporary security group...")
|
||||
if _, err := ec2conn.AuthorizeSecurityGroup(groupResp.SecurityGroup, perms); err != nil {
|
||||
err := fmt.Errorf("Error creating temporary security group: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
@@ -65,6 +67,7 @@ func (s *stepSecurityGroup) Cleanup(state map[string]interface{}) {
|
||||
ui.Say("Deleting temporary security group...")
|
||||
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.groupId})
|
||||
if err != nil {
|
||||
log.Printf("Error deleting security group: %s", err)
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up security group. Please delete the group manually: %s", s.groupId))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -17,16 +18,18 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio
|
||||
ui.Say("Stopping the source instance...")
|
||||
_, err := ec2conn.StopInstances(instance.InstanceId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error stopping instance: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Wait for the instance to actual stop
|
||||
// TODO(mitchellh): Handle diff source states, i.e. this force state sucks
|
||||
ui.Say("Waiting for the instance to stop...")
|
||||
instance.State.Name = "stopping"
|
||||
instance, err = waitForState(ec2conn, instance, []string{"running", "stopping"}, "stopped")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance to stop: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -86,6 +86,8 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
if url.Scheme == "file" && !d.config.CopyFile {
|
||||
finalPath = url.Path
|
||||
} else {
|
||||
finalPath = d.config.TargetPath
|
||||
|
||||
var ok bool
|
||||
d.downloader, ok = d.config.DownloaderMap[url.Scheme]
|
||||
if !ok {
|
||||
@@ -93,7 +95,7 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
}
|
||||
|
||||
// Otherwise, download using the downloader.
|
||||
f, err := os.Create(d.config.TargetPath)
|
||||
f, err := os.Create(finalPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,12 @@ func MultistepDebugFn(ui packer.Ui) multistep.DebugPauseFn {
|
||||
|
||||
result := make(chan string, 1)
|
||||
go func() {
|
||||
result <- ui.Ask(message)
|
||||
line, err := ui.Ask(message)
|
||||
if err != nil {
|
||||
log.Printf("Error asking for input: %s", err)
|
||||
}
|
||||
|
||||
result <- line
|
||||
}()
|
||||
|
||||
for {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// All of the methods used to communicate with the digital_ocean API
|
||||
// are here. Their API is on a path to V2, so just plain JSON is used
|
||||
// in place of a proper client library for now.
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
|
||||
|
||||
type Image struct {
|
||||
Id uint
|
||||
Name string
|
||||
Distribution string
|
||||
}
|
||||
|
||||
type ImagesResp struct {
|
||||
Images []Image
|
||||
}
|
||||
|
||||
type DigitalOceanClient struct {
|
||||
// The http client for communicating
|
||||
client *http.Client
|
||||
|
||||
// The base URL of the API
|
||||
BaseURL string
|
||||
|
||||
// Credentials
|
||||
ClientID string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: http.DefaultClient,
|
||||
BaseURL: DIGITALOCEAN_API_URL,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Creates an SSH Key and returns it's id
|
||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
// Escape the public key
|
||||
pub = url.QueryEscape(pub)
|
||||
|
||||
params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub)
|
||||
|
||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the SSH key's ID we just created
|
||||
key := body["ssh_key"].(map[string]interface{})
|
||||
keyId := key["id"].(float64)
|
||||
return uint(keyId), nil
|
||||
}
|
||||
|
||||
// Destroys an SSH key
|
||||
func (d DigitalOceanClient) DestroyKey(id uint) error {
|
||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a droplet and returns it's id
|
||||
func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) {
|
||||
params := fmt.Sprintf(
|
||||
"name=%v&image_id=%v&size_id=%v®ion_id=%v&ssh_key_ids=%v",
|
||||
name, image, size, region, keyId)
|
||||
|
||||
body, err := NewRequest(d, "droplets/new", params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Read the Droplets ID
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
dropletId := droplet["id"].(float64)
|
||||
return uint(dropletId), err
|
||||
}
|
||||
|
||||
// Destroys a droplet
|
||||
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Powers off a droplet
|
||||
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
||||
|
||||
_, err := NewRequest(d, path, "")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a snaphot of a droplet by it's ID
|
||||
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
||||
params := fmt.Sprintf("name=%v", name)
|
||||
|
||||
_, err := NewRequest(d, path, params)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns all available images.
|
||||
func (d DigitalOceanClient) Images() ([]Image, error) {
|
||||
resp, err := NewRequest(d, "images", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result ImagesResp
|
||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Images, nil
|
||||
}
|
||||
|
||||
// Destroys an image by its ID.
|
||||
func (d DigitalOceanClient) DestroyImage(id uint) error {
|
||||
path := fmt.Sprintf("images/%d/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
||||
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
||||
path := fmt.Sprintf("droplets/%v", id)
|
||||
|
||||
body, err := NewRequest(d, path, "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var ip string
|
||||
|
||||
// Read the droplet's "status"
|
||||
droplet := body["droplet"].(map[string]interface{})
|
||||
status := droplet["status"].(string)
|
||||
|
||||
if droplet["ip_address"] != nil {
|
||||
ip = droplet["ip_address"].(string)
|
||||
}
|
||||
|
||||
return ip, status, err
|
||||
}
|
||||
|
||||
// Sends an api request and returns a generic map[string]interface of
|
||||
// the response.
|
||||
func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) {
|
||||
client := d.client
|
||||
url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s",
|
||||
DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey)
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
|
||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &decodedResponse)
|
||||
|
||||
log.Printf("response from digitalocean: %v", decodedResponse)
|
||||
|
||||
// Catch all non-200 status and return an error
|
||||
if resp.StatusCode != 200 {
|
||||
err = errors.New(fmt.Sprintf("Received non-200 HTTP status from DigitalOcean: %v", resp.StatusCode))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
// Catch all non-OK statuses from DO and return an error
|
||||
status := decodedResponse["status"]
|
||||
if status != "OK" {
|
||||
// Get the actual error message if there is one
|
||||
if status == "ERROR" {
|
||||
status = decodedResponse["error_message"]
|
||||
}
|
||||
err = errors.New(fmt.Sprintf("Received bad status from DigitalOcean: %v", status))
|
||||
return decodedResponse, err
|
||||
}
|
||||
|
||||
return decodedResponse, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
// The name of the snapshot
|
||||
snapshotName string
|
||||
|
||||
// The ID of the image
|
||||
snapshotId uint
|
||||
|
||||
// The client for making API calls
|
||||
client *DigitalOceanClient
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
func (*Artifact) Files() []string {
|
||||
// No files with DigitalOcean
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return a.snapshotName
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("A snapshot was created: %v", a.snapshotName)
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d", a.snapshotId)
|
||||
return a.client.DestroyImage(a.snapshotId)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Artifact{}
|
||||
if _, ok := raw.(packer.Artifact); !ok {
|
||||
t.Fatalf("Artifact should be artifact")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
a := &Artifact{"packer-foobar", 42, nil}
|
||||
expected := "A snapshot was created: packer-foobar"
|
||||
|
||||
if a.String() != expected {
|
||||
t.Fatalf("artifact string should match: %v", expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// The digitalocean package contains a packer.Builder implementation
|
||||
// that builds DigitalOcean images (snapshots).
|
||||
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
const BuilderId = "pearkes.digitalocean"
|
||||
|
||||
type snapshotNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// Configuration tells the builder the credentials
|
||||
// to use while communicating with DO and describes the image
|
||||
// you are creating
|
||||
type config struct {
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
||||
SnapshotName string
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHTimeout time.Duration
|
||||
EventDelay time.Duration
|
||||
StateTimeout time.Duration
|
||||
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
|
||||
RawSnapshotName string `mapstructure:"snapshot_name"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawEventDelay string `mapstructure:"event_delay"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Optional configuration with defaults
|
||||
//
|
||||
if b.config.RegionID == 0 {
|
||||
// Default to Region "New York"
|
||||
b.config.RegionID = 1
|
||||
}
|
||||
|
||||
if b.config.SizeID == 0 {
|
||||
// Default to 512mb, the smallest droplet size
|
||||
b.config.SizeID = 66
|
||||
}
|
||||
|
||||
if b.config.ImageID == 0 {
|
||||
// Default to base image "Ubuntu 12.04 x64 Server (id: 284203)"
|
||||
b.config.ImageID = 284203
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
// Default to "root". You can override this if your
|
||||
// SourceImage has a different user account then the DO default
|
||||
b.config.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
// Default to port 22 per DO default
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
// Default to 1 minute timeouts
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
if b.config.RawEventDelay == "" {
|
||||
// Default to 5 second delays after creating events
|
||||
// to allow DO to process
|
||||
b.config.RawEventDelay = "5s"
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout == "" {
|
||||
// Default to 6 minute timeouts waiting for
|
||||
// desired state. i.e waiting for droplet to become active
|
||||
b.config.RawStateTimeout = "6m"
|
||||
}
|
||||
|
||||
// A list of errors on the configuration
|
||||
errs := make([]error, 0)
|
||||
|
||||
// Required configurations that will display errors if not set
|
||||
//
|
||||
if b.config.ClientID == "" {
|
||||
errs = append(errs, errors.New("a client_id must be specified"))
|
||||
}
|
||||
|
||||
if b.config.APIKey == "" {
|
||||
errs = append(errs, errors.New("an api_key must be specified"))
|
||||
}
|
||||
|
||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
b.config.SSHTimeout = sshTimeout
|
||||
|
||||
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing event_delay: %s", err))
|
||||
}
|
||||
b.config.EventDelay = eventDelay
|
||||
|
||||
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
|
||||
}
|
||||
b.config.StateTimeout = stateTimeout
|
||||
|
||||
// Parse the name of the snapshot
|
||||
snapNameBuf := new(bytes.Buffer)
|
||||
tData := snapshotNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
|
||||
} else {
|
||||
t.Execute(snapNameBuf, tData)
|
||||
b.config.SnapshotName = snapNameBuf.String()
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &packer.MultiError{errs}
|
||||
}
|
||||
|
||||
log.Printf("Config: %+v", b.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
// Initialize the DO API client
|
||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
|
||||
|
||||
// Set up the state
|
||||
state := make(map[string]interface{})
|
||||
state["config"] = b.config
|
||||
state["client"] = client
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
new(stepCreateSSHKey),
|
||||
new(stepCreateDroplet),
|
||||
new(stepDropletInfo),
|
||||
new(stepConnectSSH),
|
||||
new(stepProvision),
|
||||
new(stepPowerOff),
|
||||
new(stepSnapshot),
|
||||
}
|
||||
|
||||
// Run the steps
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
if _, ok := state["snapshot_name"]; !ok {
|
||||
log.Println("Failed to find snapshot_name in state. Bug?")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
snapshotName: state["snapshot_name"].(string),
|
||||
snapshotId: state["snapshot_image_id"].(uint),
|
||||
client: client,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"client_id": "foo",
|
||||
"api_key": "bar",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Prepare_BadType(t *testing.T) {
|
||||
b := &Builder{}
|
||||
c := map[string]interface{}{
|
||||
"api_key": []string{},
|
||||
}
|
||||
|
||||
err := b.Prepare(c)
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_APIKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["api_key"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.APIKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.APIKey)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "api_key")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ClientID(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["client_id"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.ClientID != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.ClientID)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "client_id")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_RegionID(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RegionID != 1 {
|
||||
t.Errorf("invalid: %d", b.config.RegionID)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["region_id"] = 2
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RegionID != 2 {
|
||||
t.Errorf("invalid: %d", b.config.RegionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SizeID(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SizeID != 66 {
|
||||
t.Errorf("invalid: %d", b.config.SizeID)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["size_id"] = 67
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SizeID != 67 {
|
||||
t.Errorf("invalid: %d", b.config.SizeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ImageID(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SizeID != 66 {
|
||||
t.Errorf("invalid: %d", b.config.SizeID)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["size_id"] = 2
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SizeID != 2 {
|
||||
t.Errorf("invalid: %d", b.config.SizeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHUsername(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHUsername != "root" {
|
||||
t.Errorf("invalid: %d", b.config.SSHUsername)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["ssh_username"] = "foo"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHUsername != "foo" {
|
||||
t.Errorf("invalid: %s", b.config.SSHUsername)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout != "1m" {
|
||||
t.Errorf("invalid: %d", b.config.RawSSHTimeout)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["ssh_timeout"] = "30s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["ssh_timeout"] = "tubes"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_EventDelay(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawEventDelay != "5s" {
|
||||
t.Errorf("invalid: %d", b.config.RawEventDelay)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["event_delay"] = "10s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["event_delay"] = "tubes"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_StateTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawStateTimeout != "6m" {
|
||||
t.Errorf("invalid: %d", b.config.RawStateTimeout)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["state_timeout"] = "5m"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["state_timeout"] = "tubes"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SnapshotName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test default
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
|
||||
t.Errorf("invalid: %d", b.config.RawSnapshotName)
|
||||
}
|
||||
|
||||
// Test set
|
||||
config["snapshot_name"] = "foobarbaz"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test set with template
|
||||
config["snapshot_name"] = "{{.CreateTime}}"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
_, err = strconv.ParseInt(b.config.SnapshotName, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse int in template: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepConnectSSH struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
privateKey := state["privateKey"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
ipAddress := state["droplet_ip"]
|
||||
|
||||
// Build the keyring for authentication. This stores the private key
|
||||
// we'll use to authenticate.
|
||||
keyring := &ssh.SimpleKeychain{}
|
||||
err := keyring.AddPEMKey(privateKey)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Build the actual SSH client configuration
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
// Start trying to connect to SSH
|
||||
connected := make(chan error, 1)
|
||||
connectQuit := make(chan bool, 1)
|
||||
defer func() {
|
||||
connectQuit <- true
|
||||
}()
|
||||
|
||||
var comm packer.Communicator
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
ui.Say("Connecting to the droplet via SSH...")
|
||||
attempts := 0
|
||||
handshakeAttempts := 0
|
||||
for {
|
||||
select {
|
||||
case <-connectQuit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
attempts += 1
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:%d (attempt %d)",
|
||||
ipAddress, config.SSHPort, attempts)
|
||||
s.conn, err = net.DialTimeout(
|
||||
"tcp",
|
||||
fmt.Sprintf("%s:%d", ipAddress, config.SSHPort),
|
||||
10*time.Second)
|
||||
if err == nil {
|
||||
log.Println("TCP connection made. Attempting SSH handshake.")
|
||||
comm, err = ssh.New(s.conn, sshConfig)
|
||||
if err == nil {
|
||||
log.Println("Connected to SSH!")
|
||||
break
|
||||
}
|
||||
|
||||
handshakeAttempts += 1
|
||||
log.Printf("SSH handshake error: %s", err)
|
||||
|
||||
if handshakeAttempts > 5 {
|
||||
connected <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// A brief sleep so we're not being overly zealous attempting
|
||||
// to connect to the instance.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
connected <- nil
|
||||
}()
|
||||
|
||||
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
|
||||
timeout := time.After(config.SSHTimeout)
|
||||
|
||||
ConnectWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-connected:
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error connecting to SSH: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// We connected. Just break the loop.
|
||||
break ConnectWaitLoop
|
||||
case <-timeout:
|
||||
err := errors.New("Timeout waiting for SSH to become available.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
log.Println("Interrupt detected, quitting waiting for SSH.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the communicator on the state bag so it can be used later
|
||||
state["communicator"] = comm
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateDroplet struct {
|
||||
dropletId uint
|
||||
}
|
||||
|
||||
func (s *stepCreateDroplet) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
c := state["config"].(config)
|
||||
sshKeyId := state["ssh_key_id"].(uint)
|
||||
|
||||
ui.Say("Creating droplet...")
|
||||
|
||||
// Some random droplet name as it's temporary
|
||||
name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
|
||||
// Create the droplet based on configuration
|
||||
dropletId, err := client.CreateDroplet(name, c.SizeID, c.ImageID, c.RegionID, sshKeyId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating droplet: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// We use this in cleanup
|
||||
s.dropletId = dropletId
|
||||
|
||||
// Store the droplet id for later
|
||||
state["droplet_id"] = dropletId
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) {
|
||||
// If the dropletid isn't there, we probably never created it
|
||||
if s.dropletId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
c := state["config"].(config)
|
||||
|
||||
// Destroy the droplet we just created
|
||||
ui.Say("Destroying droplet...")
|
||||
|
||||
// Sleep arbitrarily before sending destroy request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.EventDelay)
|
||||
|
||||
err := client.DestroyDroplet(s.dropletId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'",
|
||||
DIGITALOCEAN_API_URL, s.dropletId, c.ClientID, c.APIKey)
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying droplet. Please destroy it manually: %v", curlstr))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepCreateSSHKey struct {
|
||||
keyId uint
|
||||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary ssh key for droplet...")
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||
|
||||
// ASN.1 DER encoded form
|
||||
priv_der := x509.MarshalPKCS1PrivateKey(priv)
|
||||
priv_blk := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: priv_der,
|
||||
}
|
||||
|
||||
// Set the private key in the statebag for later
|
||||
state["privateKey"] = string(pem.EncodeToMemory(&priv_blk))
|
||||
|
||||
// Marshal the public key into SSH compatible format
|
||||
pub := priv.PublicKey
|
||||
pub_sshformat := string(ssh.MarshalAuthorizedKey(&pub))
|
||||
|
||||
// The name of the public key on DO
|
||||
name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
|
||||
// Create the key!
|
||||
keyId, err := client.CreateKey(name, pub_sshformat)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// We use this to check cleanup
|
||||
s.keyId = keyId
|
||||
|
||||
log.Printf("temporary ssh key name: %s", name)
|
||||
|
||||
// Remember some state for the future
|
||||
state["ssh_key_id"] = keyId
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateSSHKey) Cleanup(state map[string]interface{}) {
|
||||
// If no key name is set, then we never created it, so just return
|
||||
if s.keyId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
c := state["config"].(config)
|
||||
|
||||
ui.Say("Deleting temporary ssh key...")
|
||||
err := client.DestroyKey(s.keyId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
||||
DIGITALOCEAN_API_URL, s.keyId, c.ClientID, c.APIKey)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up ssh key. Please delete the key manually: %v", curlstr))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepDropletInfo struct{}
|
||||
|
||||
func (s *stepDropletInfo) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
c := state["config"].(config)
|
||||
dropletId := state["droplet_id"].(uint)
|
||||
|
||||
ui.Say("Waiting for droplet to become active...")
|
||||
|
||||
err := waitForDropletState("active", dropletId, client, c)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the IP on the state for later
|
||||
ip, _, err := client.DropletStatus(dropletId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving droplet ID: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state["droplet_ip"] = ip
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepDropletInfo) Cleanup(state map[string]interface{}) {
|
||||
// no cleanup
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepPowerOff struct{}
|
||||
|
||||
func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
c := state["config"].(config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
dropletId := state["droplet_id"].(uint)
|
||||
|
||||
// Sleep arbitrarily before sending power off request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.EventDelay)
|
||||
|
||||
// Poweroff the droplet so it can be snapshot
|
||||
err := client.PowerOffDroplet(dropletId)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error powering off droplet: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Waiting for droplet to power off...")
|
||||
|
||||
err = waitForDropletState("off", dropletId, client, c)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepPowerOff) Cleanup(state map[string]interface{}) {
|
||||
// no cleanup
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepProvision struct{}
|
||||
|
||||
func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
||||
comm := state["communicator"].(packer.Communicator)
|
||||
hook := state["hook"].(packer.Hook)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
log.Println("Running the provision hook")
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (*stepProvision) Cleanup(map[string]interface{}) {}
|
||||
@@ -0,0 +1,71 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepSnapshot struct{}
|
||||
|
||||
func (s *stepSnapshot) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
c := state["config"].(config)
|
||||
dropletId := state["droplet_id"].(uint)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
|
||||
err := client.CreateSnapshot(dropletId, c.SnapshotName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Waiting for snapshot to complete...")
|
||||
err = waitForDropletState("active", dropletId, client, c)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for snapshot to complete: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
|
||||
images, err := client.Images()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
var imageId uint
|
||||
for _, image := range images {
|
||||
if image.Name == c.SnapshotName {
|
||||
imageId = image.Id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if imageId == 0 {
|
||||
err := errors.New("Couldn't find snapshot to get the image ID. Bug?")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Snapshot image ID: %d", imageId)
|
||||
|
||||
state["snapshot_image_id"] = imageId
|
||||
state["snapshot_name"] = c.SnapshotName
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepSnapshot) Cleanup(state map[string]interface{}) {
|
||||
// no cleanup
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// a state we expect, while eventually timing out.
|
||||
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error {
|
||||
active := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
attempts := 0
|
||||
for {
|
||||
attempts += 1
|
||||
|
||||
log.Printf("Checking droplet status... (attempt: %d)", attempts)
|
||||
|
||||
_, status, err := client.DropletStatus(dropletId)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
}
|
||||
|
||||
if status == desiredState {
|
||||
break
|
||||
}
|
||||
|
||||
// Wait 3 seconds in between
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
active <- true
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState)
|
||||
timeout := time.After(c.StateTimeout)
|
||||
|
||||
ActiveWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case <-active:
|
||||
// We connected. Just break the loop.
|
||||
break ActiveWaitLoop
|
||||
case <-timeout:
|
||||
err := errors.New("Timeout while waiting to for droplet to become active")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, there were no errors
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Artifact is the result of running the VirtualBox builder, namely a set
|
||||
// of files associated with the resulting machine.
|
||||
type Artifact struct {
|
||||
dir string
|
||||
f []string
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
func (a *Artifact) Files() []string {
|
||||
return a.f
|
||||
}
|
||||
|
||||
func (*Artifact) Id() string {
|
||||
return "VM"
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -24,24 +25,28 @@ type Builder struct {
|
||||
}
|
||||
|
||||
type config struct {
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
BootWait time.Duration ``
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
ISOMD5 string `mapstructure:"iso_md5"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHWaitTimeout time.Duration ``
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
BootWait time.Duration ``
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
ISOMD5 string `mapstructure:"iso_md5"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHWaitTimeout time.Duration ``
|
||||
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
|
||||
VBoxManage [][]string `mapstructure:"vboxmanage"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
|
||||
@@ -60,6 +65,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.DiskSize == 0 {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsPath == "" {
|
||||
b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso"
|
||||
}
|
||||
|
||||
if b.config.GuestOSType == "" {
|
||||
b.config.GuestOSType = "Other"
|
||||
}
|
||||
@@ -88,6 +101,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.VBoxManage == nil {
|
||||
b.config.VBoxManage = make([][]string, 0)
|
||||
}
|
||||
|
||||
if b.config.VBoxVersionFile == "" {
|
||||
b.config.VBoxVersionFile = ".vbox_version"
|
||||
}
|
||||
|
||||
if b.config.VMName == "" {
|
||||
b.config.VMName = "packer"
|
||||
}
|
||||
@@ -116,7 +137,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
if url.Scheme == "file" {
|
||||
if _, err := os.Stat(b.config.ISOUrl); err != nil {
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
|
||||
}
|
||||
} else {
|
||||
@@ -143,6 +164,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(b.config.OutputDir); err == nil {
|
||||
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
|
||||
}
|
||||
|
||||
if b.config.RawBootWait != "" {
|
||||
b.config.BootWait, err = time.ParseDuration(b.config.RawBootWait)
|
||||
if err != nil {
|
||||
@@ -190,6 +215,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
steps := []multistep.Step{
|
||||
new(stepDownloadGuestAdditions),
|
||||
new(stepDownloadISO),
|
||||
new(stepPrepareOutputDir),
|
||||
new(stepHTTPServer),
|
||||
@@ -198,9 +224,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
new(stepCreateDisk),
|
||||
new(stepAttachISO),
|
||||
new(stepForwardSSH),
|
||||
new(stepVBoxManage),
|
||||
new(stepRun),
|
||||
new(stepTypeBootCommand),
|
||||
new(stepWaitForSSH),
|
||||
new(stepUploadVersion),
|
||||
new(stepUploadGuestAdditions),
|
||||
new(stepProvision),
|
||||
new(stepShutdown),
|
||||
new(stepExport),
|
||||
@@ -226,7 +255,40 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
return nil, nil
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If we were interrupted or cancelled, then just exit.
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
return nil, errors.New("Build was cancelled.")
|
||||
}
|
||||
|
||||
if _, ok := state[multistep.StateHalted]; ok {
|
||||
return nil, errors.New("Build was halted.")
|
||||
}
|
||||
|
||||
// Compile the artifact list
|
||||
files := make([]string, 0, 5)
|
||||
visit := func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
dir: b.config.OutputDir,
|
||||
f: files,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -75,6 +76,58 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_DiskSize(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "disk_size")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.DiskSize != 40000 {
|
||||
t.Fatalf("bad size: %d", b.config.DiskSize)
|
||||
}
|
||||
|
||||
config["disk_size"] = 60000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.DiskSize != 60000 {
|
||||
t.Fatalf("bad size: %s", b.config.DiskSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "disk_size")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsPath != "VBoxGuestAdditions.iso" {
|
||||
t.Fatalf("bad: %s", b.config.GuestAdditionsPath)
|
||||
}
|
||||
|
||||
config["guest_additions_path"] = "foo"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsPath != "foo" {
|
||||
t.Fatalf("bad size: %s", b.config.GuestAdditionsPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -171,6 +224,31 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test with existing dir
|
||||
dir, err := ioutil.TempDir("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
config["output_directory"] = dir
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["output_directory"] = "i-hope-i-dont-exist"
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -253,3 +331,66 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_VBoxManage(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test with empty
|
||||
delete(config, "vboxmanage")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(b.config.VBoxManage, [][]string{}) {
|
||||
t.Fatalf("bad: %#v", b.config.VBoxManage)
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["vboxmanage"] = [][]interface{}{
|
||||
[]interface{}{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := [][]string{
|
||||
[]string{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(b.config.VBoxManage, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.VBoxManage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_VBoxVersionFile(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test empty
|
||||
delete(config, "virtualbox_version_file")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.VBoxVersionFile != ".vbox_version" {
|
||||
t.Fatalf("bad value: %s", b.config.VBoxVersionFile)
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["virtualbox_version_file"] = "foo"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.VBoxVersionFile != "foo" {
|
||||
t.Fatalf("bad value: %s", b.config.VBoxVersionFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -29,6 +30,9 @@ type Driver interface {
|
||||
// properly. If there is any indication the driver can't function,
|
||||
// this will return an error.
|
||||
Verify() error
|
||||
|
||||
// Version reads the version of VirtualBox that is installed.
|
||||
Version() (string, error)
|
||||
}
|
||||
|
||||
type VBox42Driver struct {
|
||||
@@ -49,6 +53,12 @@ func (d *VBox42Driver) IsRunning(name string) (bool, error) {
|
||||
if line == `VMState="running"` {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We consider "stopping" to still be running. We wait for it to
|
||||
// be completely stopped or some other state.
|
||||
if line == `VMState="stopping"` {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
@@ -88,8 +98,15 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
|
||||
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
|
||||
stdoutString := strings.TrimSpace(stdout.String())
|
||||
stderrString := strings.TrimSpace(stderr.String())
|
||||
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
err = fmt.Errorf("VBoxManage error: %s", stderrString)
|
||||
}
|
||||
|
||||
log.Printf("stdout: %s", stdoutString)
|
||||
log.Printf("stderr: %s", stderrString)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -97,3 +114,21 @@ func (d *VBox42Driver) VBoxManage(args ...string) error {
|
||||
func (d *VBox42Driver) Verify() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VBox42Driver) Version() (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
|
||||
cmd := exec.Command(d.VBoxManagePath, "--version")
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
versionRe := regexp.MustCompile("[^.0-9]")
|
||||
matches := versionRe.Split(stdout.String(), 2)
|
||||
if len(matches) == 0 {
|
||||
return "", fmt.Errorf("No version found: %s", stdout.String())
|
||||
}
|
||||
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ func (s *stepAttachISO) Run(state map[string]interface{}) multistep.StepAction {
|
||||
"--medium", isoPath,
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error attaching hard drive: %s", err))
|
||||
err := fmt.Errorf("Error attaching ISO: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
|
||||
command := []string{
|
||||
"createhd",
|
||||
"--filename", path,
|
||||
"--size", "40000",
|
||||
"--size", strconv.FormatUint(uint64(config.DiskSize), 10),
|
||||
"--format", format,
|
||||
"--variant", "Standard",
|
||||
}
|
||||
@@ -32,7 +33,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
|
||||
ui.Say("Creating hard drive...")
|
||||
err := driver.VBoxManage(command...)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating hard drive: %s", err))
|
||||
err := fmt.Errorf("Error creating hard drive: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -40,7 +43,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
|
||||
controllerName := "IDE Controller"
|
||||
err = driver.VBoxManage("storagectl", vmName, "--name", controllerName, "--add", "ide")
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating disk controller: %s", err))
|
||||
err := fmt.Errorf("Error creating disk controller: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -54,7 +59,9 @@ func (s *stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction
|
||||
"--medium", path,
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error attaching hard drive: %s", err))
|
||||
err := fmt.Errorf("Error attaching hard drive: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ func (s *stepCreateVM) Run(state map[string]interface{}) multistep.StepAction {
|
||||
for _, command := range commands {
|
||||
err := driver.VBoxManage(command...)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating VM: %s", err))
|
||||
err := fmt.Errorf("Error creating VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var additionsVersionMap = map[string]string{
|
||||
"4.2.1": "4.2.0",
|
||||
"4.1.23": "4.1.22",
|
||||
}
|
||||
|
||||
// This step uploads a file containing the VirtualBox version, which
|
||||
// can be useful for various provisioning reasons.
|
||||
//
|
||||
// Produces:
|
||||
// guest_additions_path string - Path to the guest additions.
|
||||
type stepDownloadGuestAdditions struct{}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
|
||||
var action multistep.StepAction
|
||||
cache := state["cache"].(packer.Cache)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
version, err := driver.Version()
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error reading version for guest additions download: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if newVersion, ok := additionsVersionMap[version]; ok {
|
||||
log.Printf("Rewriting guest additions version: %s to %s", version, newVersion)
|
||||
version = newVersion
|
||||
}
|
||||
|
||||
// First things first, we get the list of checksums for the files available
|
||||
// for this version.
|
||||
checksumsUrl := fmt.Sprintf("http://download.virtualbox.org/virtualbox/%s/SHA256SUMS", version)
|
||||
checksumsFile, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf(
|
||||
"Failed creating temporary file to store guest addition checksums: %s",
|
||||
err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
checksumsFile.Close()
|
||||
defer os.Remove(checksumsFile.Name())
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: checksumsUrl,
|
||||
TargetPath: checksumsFile.Name(),
|
||||
Hash: nil,
|
||||
}
|
||||
|
||||
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
checksumsPath, action := s.progressDownload(download, state)
|
||||
if action != multistep.ActionContinue {
|
||||
return action
|
||||
}
|
||||
|
||||
additionsName := fmt.Sprintf("VBoxGuestAdditions_%s.iso", version)
|
||||
|
||||
// Next, we find the checksum for the file we're looking to download.
|
||||
// It is an error if the checksum cannot be found.
|
||||
checksumsF, err := os.Open(checksumsPath)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer checksumsF.Close()
|
||||
|
||||
// We copy the contents of the file into memory. In general this file
|
||||
// is quite small so that is okay. In the future, we probably want to
|
||||
// use bufio and iterate line by line.
|
||||
var contents bytes.Buffer
|
||||
io.Copy(&contents, checksumsF)
|
||||
|
||||
checksum := ""
|
||||
for _, line := range strings.Split(contents.String(), "\n") {
|
||||
parts := strings.Fields(line)
|
||||
log.Printf("Checksum file parts: %#v", parts)
|
||||
if len(parts) != 2 {
|
||||
// Bogus line
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(parts[1], additionsName) {
|
||||
checksum = parts[0]
|
||||
log.Printf("Guest additions checksum: %s", checksum)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if checksum == "" {
|
||||
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
checksumBytes, err := hex.DecodeString(checksum)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Couldn't decode checksum into bytes: %s", checksum)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"http://download.virtualbox.org/virtualbox/%s/%s",
|
||||
version, additionsName)
|
||||
log.Printf("Guest additions URL: %s", url)
|
||||
|
||||
log.Printf("Acquiring lock to download the guest additions ISO.")
|
||||
cachePath := cache.Lock(url)
|
||||
defer cache.Unlock(url)
|
||||
|
||||
downloadConfig = &common.DownloadConfig{
|
||||
Url: url,
|
||||
TargetPath: cachePath,
|
||||
Hash: sha256.New(),
|
||||
Checksum: checksumBytes,
|
||||
}
|
||||
|
||||
download = common.NewDownloadClient(downloadConfig)
|
||||
ui.Say("Downloading VirtualBox guest additions. Progress will be shown periodically.")
|
||||
state["guest_additions_path"], action = s.progressDownload(download, state)
|
||||
return action
|
||||
}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) Cleanup(state map[string]interface{}) {}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) progressDownload(c *common.DownloadClient, state map[string]interface{}) (string, multistep.StepAction) {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var result string
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
|
||||
// Start a goroutine to actually do the download...
|
||||
go func() {
|
||||
var err error
|
||||
result, err = c.Get()
|
||||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
// A loop that handles showing progress as well as timing out and handling
|
||||
// interrupts and all that.
|
||||
DownloadWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error downloading: %s", err)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
case <-progressTicker.C:
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", c.PercentProgress()))
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, multistep.ActionContinue
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,16 +24,20 @@ import (
|
||||
//
|
||||
// Produces:
|
||||
// iso_path string
|
||||
type stepDownloadISO struct{}
|
||||
type stepDownloadISO struct {
|
||||
isoCopyDir string
|
||||
}
|
||||
|
||||
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
|
||||
func (s *stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
|
||||
cache := state["cache"].(packer.Cache)
|
||||
config := state["config"].(*config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
checksum, err := hex.DecodeString(config.ISOMD5)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error parsing checksum: %s", err))
|
||||
err := fmt.Errorf("Error parsing checksum: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -62,12 +70,15 @@ DownloadWaitLoop:
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error downloading ISO: %s", err))
|
||||
err := fmt.Errorf("Error downloading ISO: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
case <-progressTicker.C:
|
||||
ui.Say(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
@@ -76,10 +87,43 @@ DownloadWaitLoop:
|
||||
}
|
||||
}
|
||||
|
||||
// VirtualBox is really dumb and can't figure out that the file is an
|
||||
// ISO unless it has a ".iso" extension. We can't modify the cache
|
||||
// filenames so we just do a copy.
|
||||
tempdir, err := ioutil.TempDir("", "packer")
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
s.isoCopyDir = tempdir
|
||||
|
||||
f, err := os.Create(filepath.Join(tempdir, "image.iso"))
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sourceF, err := os.Open(cachePath)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer sourceF.Close()
|
||||
|
||||
if _, err := io.Copy(f, sourceF); err != nil {
|
||||
state["error"] = fmt.Errorf("Error copying ISO: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Path to ISO on disk: %s", cachePath)
|
||||
state["iso_path"] = cachePath
|
||||
state["iso_path"] = f.Name()
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
|
||||
func (s *stepDownloadISO) Cleanup(map[string]interface{}) {
|
||||
if s.isoCopyDir != "" {
|
||||
os.RemoveAll(s.isoCopyDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui.Say("Exporting virtual machine...")
|
||||
err := driver.VBoxManage(command...)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error exporting virtual machine: %s", err))
|
||||
err := fmt.Errorf("Error exporting virtual machine: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ func (s *stepForwardSSH) Run(state map[string]interface{}) multistep.StepAction
|
||||
fmt.Sprintf("packerssh,tcp,127.0.0.1,%d,,%d", sshHostPort, config.SSHPort),
|
||||
}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating port forwarding rule: %s", err))
|
||||
err := fmt.Errorf("Error creating port forwarding rule: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package virtualbox
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -11,10 +12,22 @@ func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepActi
|
||||
config := state["config"].(*config)
|
||||
|
||||
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepPrepareOutputDir) Cleanup(map[string]interface{}) {}
|
||||
func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
|
||||
_, cancelled := state[multistep.StateCancelled]
|
||||
_, halted := state[multistep.StateHalted]
|
||||
|
||||
if cancelled || halted {
|
||||
config := state["config"].(*config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting output directory...")
|
||||
os.RemoveAll(config.OutputDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
log.Println("Running the provision hook")
|
||||
hook.Run(packer.HookProvision, ui, comm, nil)
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui.Say("Starting the virtual machine...")
|
||||
command := []string{"startvm", vmName, "--type", "gui"}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error starting VM: %s", err))
|
||||
err := fmt.Errorf("Error starting VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -46,7 +48,10 @@ func (s *stepRun) Cleanup(state map[string]interface{}) {
|
||||
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
if err := driver.VBoxManage("controlvm", s.vmName, "poweroff"); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
|
||||
|
||||
if running, _ := driver.IsRunning(s.vmName); running {
|
||||
if err := driver.VBoxManage("controlvm", s.vmName, "poweroff"); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -34,7 +35,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
|
||||
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
|
||||
if err := comm.Start(cmd); err != nil {
|
||||
ui.Error(fmt.Sprintf("Failed to send shutdown command: %s", err))
|
||||
err := fmt.Errorf("Failed to send shutdown command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -52,7 +55,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
select {
|
||||
case <-shutdownTimer:
|
||||
ui.Error("Timeout while waiting for machine to shut down.")
|
||||
err := errors.New("Timeout while waiting for machine to shut down.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -61,7 +66,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
} else {
|
||||
ui.Say("Halting the virtual machine...")
|
||||
if err := driver.Stop(vmName); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error stopping VM: %s", err))
|
||||
err := fmt.Errorf("Error stopping VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ func (stepSuppressMessages) Run(state map[string]interface{}) multistep.StepActi
|
||||
|
||||
log.Println("Suppressing annoying messages in VirtualBox")
|
||||
if err := driver.SuppressMessages(); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error configuring VirtualBox to suppress messages: %s", err))
|
||||
err := fmt.Errorf("Error configuring VirtualBox to suppress messages: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
}
|
||||
|
||||
if err := driver.VBoxManage("controlvm", vmName, "keyboardputscancode", code); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error sending boot command: %s", err))
|
||||
err := fmt.Errorf("Error sending boot command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
@@ -80,8 +82,9 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
|
||||
func scancodes(message string) []string {
|
||||
special := make(map[string][]string)
|
||||
special["<enter>"] = []string{"1c", "9c"}
|
||||
special["<return>"] = []string{"1c", "9c"}
|
||||
special["<esc>"] = []string{"01", "81"}
|
||||
special["<return>"] = []string{"1c", "9c"}
|
||||
special["<tab>"] = []string{"0f", "8f"}
|
||||
|
||||
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type guestAdditionsPathTemplate struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// This step uploads the guest additions ISO to the VM.
|
||||
type stepUploadGuestAdditions struct{}
|
||||
|
||||
func (s *stepUploadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
|
||||
comm := state["communicator"].(packer.Communicator)
|
||||
config := state["config"].(*config)
|
||||
driver := state["driver"].(Driver)
|
||||
guestAdditionsPath := state["guest_additions_path"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
version, err := driver.Version()
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error reading version for guest additions upload: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
f, err := os.Open(guestAdditionsPath)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error opening guest additions ISO: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
tplData := &guestAdditionsPathTemplate{
|
||||
Version: version,
|
||||
}
|
||||
|
||||
var processedPath bytes.Buffer
|
||||
t := template.Must(template.New("path").Parse(config.GuestAdditionsPath))
|
||||
t.Execute(&processedPath, tplData)
|
||||
|
||||
ui.Say("Uploading VirtualBox guest additions ISO...")
|
||||
if err := comm.Upload(processedPath.String(), f); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading guest additions: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepUploadGuestAdditions) Cleanup(state map[string]interface{}) {}
|
||||
@@ -0,0 +1,43 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// This step uploads a file containing the VirtualBox version, which
|
||||
// can be useful for various provisioning reasons.
|
||||
type stepUploadVersion struct{}
|
||||
|
||||
func (s *stepUploadVersion) Run(state map[string]interface{}) multistep.StepAction {
|
||||
comm := state["communicator"].(packer.Communicator)
|
||||
config := state["config"].(*config)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
if config.VBoxVersionFile == "" {
|
||||
log.Println("VBoxVersionFile is empty. Not uploading.")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
version, err := driver.Version()
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error reading version for metadata upload: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Uploading VirtualBox version info (%s)", version))
|
||||
var data bytes.Buffer
|
||||
data.WriteString(version)
|
||||
if err := comm.Upload(config.VBoxVersionFile, &data); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading VirtualBox version: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepUploadVersion) Cleanup(state map[string]interface{}) {}
|
||||
@@ -0,0 +1,61 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type commandTemplate struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// This step executes additional VBoxManage commands as specified by the
|
||||
// template.
|
||||
//
|
||||
// Uses:
|
||||
//
|
||||
// Produces:
|
||||
type stepVBoxManage struct{}
|
||||
|
||||
func (s *stepVBoxManage) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(*config)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
vmName := state["vmName"].(string)
|
||||
|
||||
if len(config.VBoxManage) > 0 {
|
||||
ui.Say("Executing custom VBoxManage commands...")
|
||||
}
|
||||
|
||||
tplData := &commandTemplate{
|
||||
Name: vmName,
|
||||
}
|
||||
|
||||
for _, originalCommand := range config.VBoxManage {
|
||||
command := make([]string, len(originalCommand))
|
||||
copy(command, originalCommand)
|
||||
|
||||
for i, arg := range command {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("arg").Parse(arg))
|
||||
t.Execute(&buf, tplData)
|
||||
command[i] = buf.String()
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Executing: %s", strings.Join(command, " ")))
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error executing command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepVBoxManage) Cleanup(state map[string]interface{}) {}
|
||||
@@ -1,6 +1,9 @@
|
||||
package vmware
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Artifact is the result of running the VMware builder, namely a set
|
||||
// of files associated with the resulting machine.
|
||||
@@ -24,3 +27,7 @@ func (*Artifact) Id() string {
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
return os.RemoveAll(a.dir)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type Builder struct {
|
||||
|
||||
type config struct {
|
||||
DiskName string `mapstructure:"vmdk_name"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
ISOMD5 string `mapstructure:"iso_md5"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
@@ -40,6 +41,7 @@ type config struct {
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHWaitTimeout time.Duration ``
|
||||
VMXData map[string]string `mapstructure:"vmx_data"`
|
||||
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
||||
@@ -64,6 +66,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.DiskName = "disk"
|
||||
}
|
||||
|
||||
if b.config.DiskSize == 0 {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.GuestOSType == "" {
|
||||
b.config.GuestOSType = "other"
|
||||
}
|
||||
@@ -92,6 +98,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.OutputDir = "vmware"
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
@@ -118,7 +128,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
if url.Scheme == "file" {
|
||||
if _, err := os.Stat(b.config.ISOUrl); err != nil {
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
|
||||
}
|
||||
} else {
|
||||
@@ -145,6 +155,10 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(b.config.OutputDir); err == nil {
|
||||
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
|
||||
}
|
||||
|
||||
if b.config.SSHUser == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified."))
|
||||
}
|
||||
@@ -225,15 +239,21 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// If we were interrupted or cancelled, then just exit.
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
return nil, nil
|
||||
return nil, errors.New("Build was cancelled.")
|
||||
}
|
||||
|
||||
if _, ok := state[multistep.StateHalted]; ok {
|
||||
return nil, nil
|
||||
return nil, errors.New("Build was halted.")
|
||||
}
|
||||
|
||||
// Compile the artifact list
|
||||
|
||||
@@ -68,6 +68,32 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_DiskSize(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "disk_size")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.DiskSize != 40000 {
|
||||
t.Fatalf("bad size: %d", b.config.DiskSize)
|
||||
}
|
||||
|
||||
config["disk_size"] = 60000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.DiskSize != 60000 {
|
||||
t.Fatalf("bad size: %s", b.config.DiskSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -164,6 +190,31 @@ func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test with existing dir
|
||||
dir, err := ioutil.TempDir("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
config["output_directory"] = dir
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["output_directory"] = "i-hope-i-dont-exist"
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -200,6 +251,34 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test with a bad value
|
||||
delete(config, "ssh_port")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHPort != 22 {
|
||||
t.Fatalf("bad ssh port: %d", b.config.SSHPort)
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["ssh_port"] = 44
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SSHPort != 44 {
|
||||
t.Fatalf("bad ssh port: %d", b.config.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -3,6 +3,7 @@ package vmware
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -38,7 +39,7 @@ type Fusion5Driver struct {
|
||||
|
||||
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
|
||||
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, _, err := d.runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,14 +52,13 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "list")
|
||||
cmd.Stdout = stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
stdout, _, err := d.runAndLog(cmd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(stdout.String(), "\n") {
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
if line == vmxPath {
|
||||
return true, nil
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
|
||||
|
||||
func (d *Fusion5Driver) Start(vmxPath string) error {
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, "gui")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, _, err := d.runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (d *Fusion5Driver) Start(vmxPath string) error {
|
||||
|
||||
func (d *Fusion5Driver) Stop(vmxPath string) error {
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "stop", vmxPath, "hard")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, _, err := d.runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -120,3 +120,17 @@ func (d *Fusion5Driver) vdiskManagerPath() string {
|
||||
func (d *Fusion5Driver) vmrunPath() string {
|
||||
return filepath.Join(d.AppPath, "Contents", "Library", "vmrun")
|
||||
}
|
||||
|
||||
func (d *Fusion5Driver) runAndLog(cmd *exec.Cmd) (string, string, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:])
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
err := cmd.Run()
|
||||
|
||||
log.Printf("stdout: %s", strings.TrimSpace(stdout.String()))
|
||||
log.Printf("stderr: %s", strings.TrimSpace(stderr.String()))
|
||||
|
||||
return stdout.String(), stderr.String(), err
|
||||
}
|
||||
|
||||
@@ -29,13 +29,17 @@ func (stepConfigureVNC) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
f, err := os.Open(vmxPath)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error while reading VMX data: %s", err))
|
||||
err := fmt.Errorf("Error reading VMX data: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vmxBytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error reading VMX data: %s", err))
|
||||
err := fmt.Errorf("Error reading VMX data: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -62,7 +66,9 @@ func (stepConfigureVNC) Run(state map[string]interface{}) multistep.StepAction {
|
||||
vmxData["RemoteDisplay.vnc.port"] = fmt.Sprintf("%d", vncPort)
|
||||
|
||||
if err := WriteVMX(vmxPath, vmxData); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error writing VMX data: %s", err))
|
||||
err := fmt.Errorf("Error writing VMX data: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,16 @@ import (
|
||||
type stepCreateDisk struct{}
|
||||
|
||||
func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
|
||||
// TODO(mitchellh): Configurable disk size
|
||||
// TODO(mitchellh): Capture error output in case things go wrong to report it
|
||||
|
||||
config := state["config"].(*config)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating virtual machine disk")
|
||||
output := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
|
||||
if err := driver.CreateDisk(output, "40000M"); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating VMware disk: %s", err))
|
||||
if err := driver.CreateDisk(output, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
|
||||
err := fmt.Errorf("Error creating disk: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,9 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
vmxPath := filepath.Join(config.OutputDir, config.VMName+".vmx")
|
||||
if err := WriteVMX(vmxPath, vmxData); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error creating VMX: %s", err))
|
||||
err := fmt.Errorf("Error creating VMX file: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction
|
||||
|
||||
checksum, err := hex.DecodeString(config.ISOMD5)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error parsing checksum: %s", err))
|
||||
err := fmt.Errorf("Error parsing checksum: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -62,7 +64,10 @@ DownloadWaitLoop:
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error downloading ISO: %s", err))
|
||||
err := fmt.Errorf("Error downloading ISO: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
|
||||
@@ -2,6 +2,7 @@ package vmware
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -11,10 +12,22 @@ func (stepPrepareOutputDir) Run(state map[string]interface{}) multistep.StepActi
|
||||
config := state["config"].(*config)
|
||||
|
||||
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepPrepareOutputDir) Cleanup(map[string]interface{}) {}
|
||||
func (stepPrepareOutputDir) Cleanup(state map[string]interface{}) {
|
||||
_, cancelled := state[multistep.StateCancelled]
|
||||
_, halted := state[multistep.StateHalted]
|
||||
|
||||
if cancelled || halted {
|
||||
config := state["config"].(*config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting output directory...")
|
||||
os.RemoveAll(config.OutputDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
log.Println("Running the provision hook")
|
||||
hook.Run(packer.HookProvision, ui, comm, nil)
|
||||
if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil {
|
||||
state["error"] = err
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
ui.Say("Starting virtual machine...")
|
||||
if err := driver.Start(vmxPath); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error starting VM: %s", err))
|
||||
err := fmt.Errorf("Error starting VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -34,7 +35,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
|
||||
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
|
||||
if err := comm.Start(cmd); err != nil {
|
||||
ui.Error(fmt.Sprintf("Failed to send shutdown command: %s", err))
|
||||
err := fmt.Errorf("Failed to send shutdown command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -52,7 +55,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
select {
|
||||
case <-shutdownTimer:
|
||||
ui.Error("Timeout while waiting for machine to shut down.")
|
||||
err := errors.New("Timeout while waiting for machine to shut down.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
default:
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -60,7 +65,9 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
}
|
||||
} else {
|
||||
if err := driver.Stop(vmxPath); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error stopping VM: %s", err))
|
||||
err := fmt.Errorf("Error stopping VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,14 +45,18 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
ui.Say("Connecting to VM via VNC")
|
||||
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", vncPort))
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error connecting to VNC: %s", err))
|
||||
err := fmt.Errorf("Error connecting to VNC: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer nc.Close()
|
||||
|
||||
c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: true})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error handshaking with VNC: %s", err))
|
||||
err := fmt.Errorf("Error handshaking with VNC: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer c.Close()
|
||||
@@ -63,7 +67,9 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
ipFinder := &IfconfigIPFinder{"vmnet8"}
|
||||
hostIp, err := ipFinder.HostIP()
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error detecting host IP: %s", err))
|
||||
err := fmt.Errorf("Error detecting host IP: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -90,8 +96,9 @@ func (*stepTypeBootCommand) Cleanup(map[string]interface{}) {}
|
||||
func vncSendString(c *vnc.ClientConn, original string) {
|
||||
special := make(map[string]uint32)
|
||||
special["<enter>"] = 0xFF0D
|
||||
special["<return>"] = 0xFF0D
|
||||
special["<esc>"] = 0xFF1B
|
||||
special["<return>"] = 0xFF0D
|
||||
special["<tab>"] = 0xFF09
|
||||
|
||||
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
|
||||
|
||||
|
||||
@@ -113,6 +113,8 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
|
||||
ui := state["ui"].(packer.Ui)
|
||||
vmxPath := state["vmx_path"].(string)
|
||||
|
||||
handshakeAttempts := 0
|
||||
|
||||
ui.Say("Waiting for SSH to become available...")
|
||||
var comm packer.Communicator
|
||||
var nc net.Conn
|
||||
@@ -144,7 +146,7 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
|
||||
log.Printf("Detected IP: %s", ip)
|
||||
|
||||
// Attempt to connect to SSH port
|
||||
nc, err = net.Dial("tcp", fmt.Sprintf("%s:22", ip))
|
||||
nc, err = net.Dial("tcp", fmt.Sprintf("%s:%d", ip, config.SSHPort))
|
||||
if err != nil {
|
||||
log.Printf("TCP connection to SSH ip/port failed: %s", err)
|
||||
continue
|
||||
@@ -160,6 +162,14 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
|
||||
|
||||
comm, err = ssh.New(nc, sshConfig)
|
||||
if err != nil {
|
||||
log.Printf("SSH handshake err: %s", err)
|
||||
|
||||
handshakeAttempts += 1
|
||||
if handshakeAttempts < 10 {
|
||||
// Try to connect via SSH a handful of times
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+38
-30
@@ -63,9 +63,10 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
|
||||
// The component finder for our builds
|
||||
components := &packer.ComponentFinder{
|
||||
Builder: env.Builder,
|
||||
Hook: env.Hook,
|
||||
Provisioner: env.Provisioner,
|
||||
Builder: env.Builder,
|
||||
Hook: env.Hook,
|
||||
PostProcessor: env.PostProcessor,
|
||||
Provisioner: env.Provisioner,
|
||||
}
|
||||
|
||||
// Go through each builder and compile the builds that we care about
|
||||
@@ -127,19 +128,11 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
|
||||
buildUis := make(map[string]packer.Ui)
|
||||
for i, b := range builds {
|
||||
var ui packer.Ui
|
||||
|
||||
ui = &packer.ColoredUi{
|
||||
ui := &packer.ColoredUi{
|
||||
Color: colors[i%len(colors)],
|
||||
Ui: env.Ui(),
|
||||
}
|
||||
|
||||
ui = &packer.PrefixedUi{
|
||||
fmt.Sprintf("==> %s", b.Name()),
|
||||
fmt.Sprintf(" %s", b.Name()),
|
||||
ui,
|
||||
}
|
||||
|
||||
buildUis[b.Name()] = ui
|
||||
ui.Say(fmt.Sprintf("%s output will be in this color.", b.Name()))
|
||||
}
|
||||
@@ -163,7 +156,8 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
// Run all the builds in parallel and wait for them to complete
|
||||
var interruptWg, wg sync.WaitGroup
|
||||
interrupted := false
|
||||
artifacts := make(map[string]packer.Artifact)
|
||||
artifacts := make(map[string][]packer.Artifact)
|
||||
errors := make(map[string]error)
|
||||
for _, b := range builds {
|
||||
// Increment the waitgroup so we wait for this item to finish properly
|
||||
wg.Add(1)
|
||||
@@ -187,15 +181,17 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
go func(b packer.Build) {
|
||||
defer wg.Done()
|
||||
|
||||
var err error
|
||||
log.Printf("Starting build run: %s", b.Name())
|
||||
ui := buildUis[b.Name()]
|
||||
artifacts[b.Name()], err = b.Run(ui, env.Cache())
|
||||
name := b.Name()
|
||||
log.Printf("Starting build run: %s", name)
|
||||
ui := buildUis[name]
|
||||
runArtifacts, err := b.Run(ui, env.Cache())
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Build errored: %s", err))
|
||||
ui.Error(fmt.Sprintf("Build '%s' errored: %s", name, err))
|
||||
errors[name] = err
|
||||
} else {
|
||||
ui.Say("Build finished.")
|
||||
ui.Say(fmt.Sprintf("Build '%s' finished.", name))
|
||||
artifacts[name] = runArtifacts
|
||||
}
|
||||
}(b)
|
||||
|
||||
@@ -223,19 +219,31 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Output all the artifacts
|
||||
env.Ui().Say("\n==> The build completed! The artifacts created were:")
|
||||
for name, artifact := range artifacts {
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "--> %s: ", name)
|
||||
|
||||
if artifact != nil {
|
||||
fmt.Fprintf(&message, artifact.String())
|
||||
} else {
|
||||
fmt.Print("<nothing>")
|
||||
if len(errors) > 0 {
|
||||
env.Ui().Error("\n==> Some builds didn't complete successfully and had errors:")
|
||||
for name, err := range errors {
|
||||
env.Ui().Error(fmt.Sprintf("--> %s: %s", name, err))
|
||||
}
|
||||
}
|
||||
|
||||
env.Ui().Say(message.String())
|
||||
if len(artifacts) > 0 {
|
||||
env.Ui().Say("\n==> Builds finished. The artifacts of successful builds are:")
|
||||
for name, buildArtifacts := range artifacts {
|
||||
for _, artifact := range buildArtifacts {
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "--> %s: ", name)
|
||||
|
||||
if artifact != nil {
|
||||
fmt.Fprintf(&message, artifact.String())
|
||||
} else {
|
||||
fmt.Fprint(&message, "<nothing>")
|
||||
}
|
||||
|
||||
env.Ui().Say(message.String())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
env.Ui().Say("\n==> Builds finished but no artifacts were created.")
|
||||
}
|
||||
|
||||
return 0
|
||||
|
||||
@@ -56,15 +56,17 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
|
||||
// The component finder for our builds
|
||||
components := &packer.ComponentFinder{
|
||||
Builder: env.Builder,
|
||||
Hook: env.Hook,
|
||||
Provisioner: env.Provisioner,
|
||||
Builder: env.Builder,
|
||||
Hook: env.Hook,
|
||||
PostProcessor: env.PostProcessor,
|
||||
Provisioner: env.Provisioner,
|
||||
}
|
||||
|
||||
// Otherwise, get all the builds
|
||||
buildNames := tpl.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
log.Printf("Creating build from template for: %s", buildName)
|
||||
build, err := tpl.Build(buildName, components)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Build '%s': %s", buildName, err))
|
||||
@@ -76,6 +78,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
|
||||
// Check the configuration of all builds
|
||||
for _, b := range builds {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
err := b.Prepare()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
|
||||
@@ -34,6 +34,17 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
session.Stdout = cmd.Stdout
|
||||
session.Stderr = cmd.Stderr
|
||||
|
||||
// Request a PTY
|
||||
termModes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0, // do not echo
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
|
||||
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("starting remote command: %s", cmd.Command)
|
||||
err = session.Start(cmd.Command + "\n")
|
||||
if err != nil {
|
||||
@@ -147,5 +158,4 @@ func (c *comm) Upload(path string, input io.Reader) error {
|
||||
|
||||
func (c *comm) Download(string, io.Writer) error {
|
||||
panic("not implemented yet")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@ package ssh
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// private key for mock server
|
||||
@@ -98,38 +95,6 @@ func newMockLineServer(t *testing.T) string {
|
||||
channel.Accept()
|
||||
t.Log("Accepted channel")
|
||||
defer channel.Close()
|
||||
|
||||
data := make([]byte, 0)
|
||||
_, err = channel.Read(data)
|
||||
if err == nil {
|
||||
t.Error("should've gotten a request (exec)")
|
||||
return
|
||||
}
|
||||
|
||||
req, ok := err.(ssh.ChannelRequest)
|
||||
if !ok {
|
||||
t.Errorf("couldn't convert err to channel request: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Request != "exec" {
|
||||
t.Errorf("unexpected request type: %s", req.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Ack it
|
||||
channel.AckRequest(true)
|
||||
|
||||
// Just respond back with the payload. We trim the first 4 bytes
|
||||
// off of here because it is "\x00\x00\x00\t" and I don't really know
|
||||
// why.
|
||||
payload := strings.TrimSpace(string(req.Payload[4:]))
|
||||
response := fmt.Sprintf("ack: %s", payload)
|
||||
_, err = channel.Write([]byte(response))
|
||||
if err != nil {
|
||||
t.Errorf("error writing response: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return l.Addr().String()
|
||||
}
|
||||
@@ -184,19 +149,5 @@ func TestStart(t *testing.T) {
|
||||
cmd.Command = "echo foo"
|
||||
cmd.Stdout = stdout
|
||||
|
||||
err = client.Start(&cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("error executing command: %s", err)
|
||||
}
|
||||
|
||||
// Wait for it to complete
|
||||
t.Log("Waiting for command to complete")
|
||||
for !cmd.Exited {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Should have the correct output
|
||||
if stdout.String() != "ack: echo foo" {
|
||||
t.Fatalf("unknown output: %#v", stdout.String())
|
||||
}
|
||||
client.Start(&cmd)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/mitchellh/osext"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// This is the default, built-in configuration that ships with
|
||||
@@ -18,6 +20,7 @@ const defaultConfig = `
|
||||
|
||||
"builders": {
|
||||
"amazon-ebs": "packer-builder-amazon-ebs",
|
||||
"digitalocean": "packer-builder-digitalocean",
|
||||
"virtualbox": "packer-builder-virtualbox",
|
||||
"vmware": "packer-builder-vmware"
|
||||
},
|
||||
@@ -27,6 +30,10 @@ const defaultConfig = `
|
||||
"validate": "packer-command-validate"
|
||||
},
|
||||
|
||||
"post-processors": {
|
||||
"vagrant": "packer-post-processor-vagrant"
|
||||
},
|
||||
|
||||
"provisioners": {
|
||||
"shell": "packer-provisioner-shell"
|
||||
}
|
||||
@@ -37,9 +44,10 @@ type config struct {
|
||||
PluginMinPort uint
|
||||
PluginMaxPort uint
|
||||
|
||||
Builders map[string]string
|
||||
Commands map[string]string
|
||||
Provisioners map[string]string
|
||||
Builders map[string]string
|
||||
Commands map[string]string
|
||||
PostProcessors map[string]string `json:"post-processors"`
|
||||
Provisioners map[string]string
|
||||
}
|
||||
|
||||
// Decodes configuration in JSON format from the given io.Reader into
|
||||
@@ -52,7 +60,7 @@ func decodeConfig(r io.Reader, c *config) error {
|
||||
// Returns an array of defined command names.
|
||||
func (c *config) CommandNames() (result []string) {
|
||||
result = make([]string, 0, len(c.Commands))
|
||||
for name, _ := range c.Commands {
|
||||
for name := range c.Commands {
|
||||
result = append(result, name)
|
||||
}
|
||||
return
|
||||
@@ -91,6 +99,19 @@ func (c *config) LoadHook(name string) (packer.Hook, error) {
|
||||
return c.pluginClient(name).Hook()
|
||||
}
|
||||
|
||||
// This is a proper packer.PostProcessorFunc that can be used to load
|
||||
// packer.PostProcessor implementations from defined plugins.
|
||||
func (c *config) LoadPostProcessor(name string) (packer.PostProcessor, error) {
|
||||
log.Printf("Loading post-processor: %s", name)
|
||||
bin, ok := c.PostProcessors[name]
|
||||
if !ok {
|
||||
log.Printf("Post-processor not found: %s", name)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return c.pluginClient(bin).PostProcessor()
|
||||
}
|
||||
|
||||
// This is a proper packer.ProvisionerFunc that can be used to load
|
||||
// packer.Provisioner implementations from defined plugins.
|
||||
func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
|
||||
@@ -105,6 +126,30 @@ func (c *config) LoadProvisioner(name string) (packer.Provisioner, error) {
|
||||
}
|
||||
|
||||
func (c *config) pluginClient(path string) *plugin.Client {
|
||||
originalPath := path
|
||||
|
||||
// First attempt to find the executable by consulting the PATH.
|
||||
path, err := exec.LookPath(path)
|
||||
if err != nil {
|
||||
// If that doesn't work, look for it in the same directory
|
||||
// as the `packer` executable (us).
|
||||
log.Printf("Plugin could not be found. Checking same directory as executable.")
|
||||
exePath, err := osext.Executable()
|
||||
if err != nil {
|
||||
log.Printf("Couldn't get current exe path: %s", err)
|
||||
} else {
|
||||
log.Printf("Current exe path: %s", exePath)
|
||||
path = filepath.Join(filepath.Dir(exePath), filepath.Base(originalPath))
|
||||
}
|
||||
}
|
||||
|
||||
// If everything failed, just use the original path and let the error
|
||||
// bubble through.
|
||||
if path == "" {
|
||||
path = originalPath
|
||||
}
|
||||
|
||||
log.Printf("Creating plugin client for path: %s", path)
|
||||
var config plugin.ClientConfig
|
||||
config.Cmd = exec.Command(path)
|
||||
config.Managed = true
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
// ConfigFile returns the default path to the configuration file. On
|
||||
// Unix-like systems this is the ".packerconfig" file in the home directory.
|
||||
// On Windows, this is the "packer.config" file in the application data
|
||||
// directory.
|
||||
func ConfigFile() (string, error) {
|
||||
return configFile()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// +build darwin freebsd linux netbsd openbsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func configFile() (string, error) {
|
||||
dir, err := configDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(dir, ".packerconfig"), nil
|
||||
}
|
||||
|
||||
func configDir() (string, error) {
|
||||
// First prefer the HOME environmental variable
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
log.Printf("Detected home directory from env var: %s", home)
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// If that fails, try the shell
|
||||
var stdout bytes.Buffer
|
||||
cmd := exec.Command("sh", "-c", "eval echo ~$USER")
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(stdout.String())
|
||||
if result == "" {
|
||||
return "", errors.New("blank output")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
shell = syscall.MustLoadDLL("Shell32.dll")
|
||||
getFolderPath = shell.MustFindProc("SHGetFolderPathW")
|
||||
)
|
||||
|
||||
const CSIDL_APPDATA = 26
|
||||
|
||||
func configFile() (string, error) {
|
||||
dir, err := configDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(dir, "packer.config"), nil
|
||||
}
|
||||
|
||||
func configDir() (string, error) {
|
||||
b := make([]uint16, syscall.MAX_PATH)
|
||||
|
||||
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181(v=vs.85).aspx
|
||||
r, _, err := getFolderPath.Call(0, CSIDL_APPDATA, 0, 0, uintptr(unsafe.Pointer(&b[0])))
|
||||
if uint32(r) != 0 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return syscall.UTF16ToString(b), nil
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
@@ -36,25 +35,34 @@ func main() {
|
||||
|
||||
log.Printf("Packer config: %+v", config)
|
||||
|
||||
defer plugin.CleanupClients()
|
||||
|
||||
var cache packer.Cache
|
||||
if cacheDir := os.Getenv("PACKER_CACHE_DIR"); cacheDir != "" {
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Printf("Setting cache directory: %s", cacheDir)
|
||||
cache = &packer.FileCache{CacheDir: cacheDir}
|
||||
cacheDir := os.Getenv("PACKER_CACHE_DIR")
|
||||
if cacheDir == "" {
|
||||
cacheDir = "packer_cache"
|
||||
}
|
||||
|
||||
cacheDir, err = filepath.Abs(cacheDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Printf("Setting cache directory: %s", cacheDir)
|
||||
cache := &packer.FileCache{CacheDir: cacheDir}
|
||||
|
||||
defer plugin.CleanupClients()
|
||||
|
||||
envConfig := packer.DefaultEnvironmentConfig()
|
||||
envConfig.Cache = cache
|
||||
envConfig.Commands = config.CommandNames()
|
||||
envConfig.Components.Builder = config.LoadBuilder
|
||||
envConfig.Components.Command = config.LoadCommand
|
||||
envConfig.Components.Hook = config.LoadHook
|
||||
envConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||
envConfig.Components.Provisioner = config.LoadProvisioner
|
||||
|
||||
env, err := packer.NewEnvironment(envConfig)
|
||||
@@ -82,19 +90,23 @@ func loadConfig() (*config, error) {
|
||||
}
|
||||
|
||||
mustExist := true
|
||||
configFile := os.Getenv("PACKER_CONFIG")
|
||||
if configFile == "" {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configFile = filepath.Join(u.HomeDir, ".packerconfig")
|
||||
configFilePath := os.Getenv("PACKER_CONFIG")
|
||||
if configFilePath == "" {
|
||||
var err error
|
||||
configFilePath, err = configFile()
|
||||
mustExist = false
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error detecing default config file path: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Attempting to open config file: %s", configFile)
|
||||
f, err := os.Open(configFile)
|
||||
if configFilePath == "" {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
log.Printf("Attempting to open config file: %s", configFilePath)
|
||||
f, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
|
||||
@@ -24,4 +24,9 @@ type Artifact interface {
|
||||
// Returns human-readable output that describes the artifact created.
|
||||
// This is used for UI output. It can be multiple lines.
|
||||
String() string
|
||||
|
||||
// Destroy deletes the artifact. Packer calls this for various reasons,
|
||||
// such as if a post-processor has processed this artifact and it is
|
||||
// no longer needed.
|
||||
Destroy() error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package packer
|
||||
|
||||
type TestArtifact struct {
|
||||
id string
|
||||
destroyCalled bool
|
||||
}
|
||||
|
||||
func (*TestArtifact) BuilderId() string {
|
||||
return "bid"
|
||||
}
|
||||
|
||||
func (*TestArtifact) Files() []string {
|
||||
return []string{"a", "b"}
|
||||
}
|
||||
|
||||
func (a *TestArtifact) Id() string {
|
||||
id := a.id
|
||||
if id == "" {
|
||||
id = "id"
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (*TestArtifact) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (a *TestArtifact) Destroy() error {
|
||||
a.destroyCalled = true
|
||||
return nil
|
||||
}
|
||||
+135
-10
@@ -1,6 +1,10 @@
|
||||
package packer
|
||||
|
||||
import "log"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This is the key in configurations that is set to "true" when Packer
|
||||
// debugging is enabled.
|
||||
@@ -20,7 +24,7 @@ type Build interface {
|
||||
|
||||
// Run runs the actual builder, returning an artifact implementation
|
||||
// of what is built. If anything goes wrong, an error is returned.
|
||||
Run(Ui, Cache) (Artifact, error)
|
||||
Run(Ui, Cache) ([]Artifact, error)
|
||||
|
||||
// Cancel will cancel a running build. This will block until the build
|
||||
// is actually completely cancelled.
|
||||
@@ -41,16 +45,28 @@ type Build interface {
|
||||
// multiple files, of course, but it should be for only a single provider
|
||||
// (such as VirtualBox, EC2, etc.).
|
||||
type coreBuild struct {
|
||||
name string
|
||||
builder Builder
|
||||
builderConfig interface{}
|
||||
hooks map[string][]Hook
|
||||
provisioners []coreBuildProvisioner
|
||||
name string
|
||||
builder Builder
|
||||
builderConfig interface{}
|
||||
builderType string
|
||||
hooks map[string][]Hook
|
||||
postProcessors [][]coreBuildPostProcessor
|
||||
provisioners []coreBuildProvisioner
|
||||
|
||||
debug bool
|
||||
l sync.Mutex
|
||||
prepareCalled bool
|
||||
}
|
||||
|
||||
// Keeps track of the post-processor and the configuration of the
|
||||
// post-processor used within a build.
|
||||
type coreBuildPostProcessor struct {
|
||||
processor PostProcessor
|
||||
processorType string
|
||||
config interface{}
|
||||
keepInputArtifact bool
|
||||
}
|
||||
|
||||
// Keeps track of the provisioner and the configuration of the provisioner
|
||||
// within the build.
|
||||
type coreBuildProvisioner struct {
|
||||
@@ -66,7 +82,13 @@ func (b *coreBuild) Name() string {
|
||||
// Prepare prepares the build by doing some initialization for the builder
|
||||
// and any hooks. This _must_ be called prior to Run.
|
||||
func (b *coreBuild) Prepare() (err error) {
|
||||
// TODO: lock
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
if b.prepareCalled {
|
||||
panic("prepare already called")
|
||||
}
|
||||
|
||||
b.prepareCalled = true
|
||||
|
||||
debugConfig := map[string]interface{}{
|
||||
@@ -91,11 +113,20 @@ func (b *coreBuild) Prepare() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the post-processors
|
||||
for _, ppSeq := range b.postProcessors {
|
||||
for _, corePP := range ppSeq {
|
||||
if err = corePP.processor.Configure(corePP.config); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Runs the actual build. Prepare must be called prior to running this.
|
||||
func (b *coreBuild) Run(ui Ui, cache Cache) (Artifact, error) {
|
||||
func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
|
||||
if !b.prepareCalled {
|
||||
panic("Prepare must be called first")
|
||||
}
|
||||
@@ -122,7 +153,101 @@ func (b *coreBuild) Run(ui Ui, cache Cache) (Artifact, error) {
|
||||
}
|
||||
|
||||
hook := &DispatchHook{hooks}
|
||||
return b.builder.Run(ui, hook, cache)
|
||||
artifacts := make([]Artifact, 0, 1)
|
||||
|
||||
// The builder just has a normal Ui, but prefixed
|
||||
builderUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s", b.Name()),
|
||||
fmt.Sprintf(" %s", b.Name()),
|
||||
originalUi,
|
||||
}
|
||||
|
||||
log.Printf("Running builder: %s", b.builderType)
|
||||
builderArtifact, err := b.builder.Run(builderUi, hook, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there was no result, don't worry about running post-processors
|
||||
// because there is nothing they can do, just return.
|
||||
if builderArtifact == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
errors := make([]error, 0)
|
||||
keepOriginalArtifact := len(b.postProcessors) == 0
|
||||
|
||||
// Run the post-processors
|
||||
PostProcessorRunSeqLoop:
|
||||
for _, ppSeq := range b.postProcessors {
|
||||
priorArtifact := builderArtifact
|
||||
for i, corePP := range ppSeq {
|
||||
ppUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s (%s)", b.Name(), corePP.processorType),
|
||||
fmt.Sprintf(" %s (%s)", b.Name(), corePP.processorType),
|
||||
originalUi,
|
||||
}
|
||||
|
||||
builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType))
|
||||
artifact, err := corePP.processor.PostProcess(ppUi, priorArtifact)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Errorf("Post-processor failed: %s", err))
|
||||
continue PostProcessorRunSeqLoop
|
||||
}
|
||||
|
||||
if artifact == nil {
|
||||
log.Println("Nil artifact, halting post-processor chain.")
|
||||
continue PostProcessorRunSeqLoop
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// This is the first post-processor. We handle deleting
|
||||
// previous artifacts a bit different because multiple
|
||||
// post-processors may be using the original and need it.
|
||||
if !keepOriginalArtifact && corePP.keepInputArtifact {
|
||||
log.Printf(
|
||||
"Flagging to keep original artifact from post-processor '%s'",
|
||||
corePP.processorType)
|
||||
keepOriginalArtifact = true
|
||||
}
|
||||
} else {
|
||||
// We have a prior artifact. If we want to keep it, we append
|
||||
// it to the results list. Otherwise, we destroy it.
|
||||
if corePP.keepInputArtifact {
|
||||
artifacts = append(artifacts, priorArtifact)
|
||||
} else {
|
||||
log.Printf("Deleting prior artifact from post-processor '%s'", corePP.processorType)
|
||||
if err := priorArtifact.Destroy(); err != nil {
|
||||
errors = append(errors, fmt.Errorf("Failed cleaning up prior artifact: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
priorArtifact = artifact
|
||||
}
|
||||
|
||||
// Add on the last artifact to the results
|
||||
if priorArtifact != nil {
|
||||
artifacts = append(artifacts, priorArtifact)
|
||||
}
|
||||
}
|
||||
|
||||
if keepOriginalArtifact {
|
||||
artifacts = append(artifacts, nil)
|
||||
copy(artifacts[1:], artifacts)
|
||||
artifacts[0] = builderArtifact
|
||||
} else {
|
||||
log.Printf("Deleting original artifact for build '%s'", b.name)
|
||||
if err := builderArtifact.Destroy(); err != nil {
|
||||
errors = append(errors, fmt.Errorf("Error destroying builder artifact: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
err = &MultiError{errors}
|
||||
}
|
||||
|
||||
return artifacts, err
|
||||
}
|
||||
|
||||
func (b *coreBuild) SetDebug(val bool) {
|
||||
|
||||
+157
-18
@@ -2,13 +2,14 @@ package packer
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testBuild() Build {
|
||||
func testBuild() *coreBuild {
|
||||
return &coreBuild{
|
||||
name: "test",
|
||||
builder: &TestBuilder{},
|
||||
builder: &TestBuilder{artifactId: "b"},
|
||||
builderConfig: 42,
|
||||
hooks: map[string][]Hook{
|
||||
"foo": []Hook{&TestHook{}},
|
||||
@@ -16,6 +17,11 @@ func testBuild() Build {
|
||||
provisioners: []coreBuildProvisioner{
|
||||
coreBuildProvisioner{&TestProvisioner{}, []interface{}{42}},
|
||||
},
|
||||
postProcessors: [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,17 +42,41 @@ func TestBuild_Prepare(t *testing.T) {
|
||||
debugFalseConfig := map[string]interface{}{DebugConfigKey: false}
|
||||
|
||||
build := testBuild()
|
||||
coreB := build.(*coreBuild)
|
||||
builder := coreB.builder.(*TestBuilder)
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.Prepare()
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, debugFalseConfig}, "prepare config should be 42")
|
||||
|
||||
coreProv := coreB.provisioners[0]
|
||||
coreProv := build.provisioners[0]
|
||||
prov := coreProv.provisioner.(*TestProvisioner)
|
||||
assert.True(prov.prepCalled, "prepare should be called")
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, debugFalseConfig}, "prepare should be called with proper config")
|
||||
|
||||
corePP := build.postProcessors[0][0]
|
||||
pp := corePP.processor.(*TestPostProcessor)
|
||||
assert.True(pp.configCalled, "config should be called")
|
||||
assert.Equal(pp.configVal, 42, "config should have right value")
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
build := testBuild()
|
||||
if err := build.Prepare(); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
p := recover()
|
||||
if p == nil {
|
||||
t.Fatalf("should've paniced")
|
||||
}
|
||||
|
||||
if p.(string) != "prepare already called" {
|
||||
t.Fatalf("Invalid panic: %s", p)
|
||||
}
|
||||
}()
|
||||
|
||||
build.Prepare()
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
@@ -55,15 +85,14 @@ func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
debugConfig := map[string]interface{}{DebugConfigKey: true}
|
||||
|
||||
build := testBuild()
|
||||
coreB := build.(*coreBuild)
|
||||
builder := coreB.builder.(*TestBuilder)
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.SetDebug(true)
|
||||
build.Prepare()
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, debugConfig}, "prepare config should be 42")
|
||||
|
||||
coreProv := coreB.provisioners[0]
|
||||
coreProv := build.provisioners[0]
|
||||
prov := coreProv.provisioner.(*TestProvisioner)
|
||||
assert.True(prov.prepCalled, "prepare should be called")
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, debugConfig}, "prepare should be called with proper config")
|
||||
@@ -77,27 +106,139 @@ func TestBuild_Run(t *testing.T) {
|
||||
|
||||
build := testBuild()
|
||||
build.Prepare()
|
||||
build.Run(ui, cache)
|
||||
|
||||
coreB := build.(*coreBuild)
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
assert.Nil(err, "should not error")
|
||||
assert.Equal(len(artifacts), 2, "should have two artifacts")
|
||||
|
||||
// Verify builder was run
|
||||
builder := coreB.builder.(*TestBuilder)
|
||||
builder := build.builder.(*TestBuilder)
|
||||
assert.True(builder.runCalled, "run should be called")
|
||||
assert.Equal(builder.runUi, ui, "run should be called with ui")
|
||||
|
||||
// Verify hooks are disapatchable
|
||||
dispatchHook := builder.runHook
|
||||
dispatchHook.Run("foo", nil, nil, 42)
|
||||
|
||||
hook := coreB.hooks["foo"][0].(*TestHook)
|
||||
hook := build.hooks["foo"][0].(*TestHook)
|
||||
assert.True(hook.runCalled, "run should be called")
|
||||
assert.Equal(hook.runData, 42, "should have correct data")
|
||||
|
||||
// Verify provisioners run
|
||||
dispatchHook.Run(HookProvision, nil, nil, 42)
|
||||
prov := coreB.provisioners[0].provisioner.(*TestProvisioner)
|
||||
prov := build.provisioners[0].provisioner.(*TestProvisioner)
|
||||
assert.True(prov.provCalled, "provision should be called")
|
||||
|
||||
// Verify post-processor was run
|
||||
pp := build.postProcessors[0][0].processor.(*TestPostProcessor)
|
||||
assert.True(pp.ppCalled, "post processor should be called")
|
||||
}
|
||||
|
||||
func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
cache := &TestCache{}
|
||||
ui := testUi()
|
||||
|
||||
// Test case: Test that with no post-processors, we only get the
|
||||
// main build.
|
||||
build := testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{}
|
||||
|
||||
build.Prepare()
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expectedIds := []string{"b"}
|
||||
artifactIds := make([]string, len(artifacts))
|
||||
for i, artifact := range artifacts {
|
||||
artifactIds[i] = artifact.Id()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(artifactIds, expectedIds) {
|
||||
t.Fatalf("unexpected ids: %#v", artifactIds)
|
||||
}
|
||||
|
||||
// Test case: Test that with a single post-processor that doesn't keep
|
||||
// inputs, only that post-processors results are returned.
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", 42, false},
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expectedIds = []string{"pp"}
|
||||
artifactIds = make([]string, len(artifacts))
|
||||
for i, artifact := range artifacts {
|
||||
artifactIds[i] = artifact.Id()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(artifactIds, expectedIds) {
|
||||
t.Fatalf("unexpected ids: %#v", artifactIds)
|
||||
}
|
||||
|
||||
// Test case: Test that with multiple post-processors, as long as one
|
||||
// keeps the original, the original is kept.
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", 42, false},
|
||||
},
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", 42, true},
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expectedIds = []string{"b", "pp1", "pp2"}
|
||||
artifactIds = make([]string, len(artifacts))
|
||||
for i, artifact := range artifacts {
|
||||
artifactIds[i] = artifact.Id()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(artifactIds, expectedIds) {
|
||||
t.Fatalf("unexpected ids: %#v", artifactIds)
|
||||
}
|
||||
|
||||
// Test case: Test that with sequences, intermediaries are kept if they
|
||||
// want to be.
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", 42, true},
|
||||
},
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", 42, false},
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expectedIds = []string{"pp1a", "pp1b", "pp2b"}
|
||||
artifactIds = make([]string, len(artifacts))
|
||||
for i, artifact := range artifacts {
|
||||
artifactIds[i] = artifact.Id()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(artifactIds, expectedIds) {
|
||||
t.Fatalf("unexpected ids: %#v", artifactIds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_RunBeforePrepare(t *testing.T) {
|
||||
@@ -118,8 +259,6 @@ func TestBuild_Cancel(t *testing.T) {
|
||||
build := testBuild()
|
||||
build.Cancel()
|
||||
|
||||
coreB := build.(*coreBuild)
|
||||
|
||||
builder := coreB.builder.(*TestBuilder)
|
||||
builder := build.builder.(*TestBuilder)
|
||||
assert.True(builder.cancelCalled, "cancel should be called")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package packer
|
||||
|
||||
type TestBuilder struct {
|
||||
artifactId string
|
||||
|
||||
prepareCalled bool
|
||||
prepareConfig []interface{}
|
||||
runCalled bool
|
||||
@@ -21,7 +23,7 @@ func (tb *TestBuilder) Run(ui Ui, h Hook, c Cache) (Artifact, error) {
|
||||
tb.runHook = h
|
||||
tb.runUi = ui
|
||||
tb.runCache = c
|
||||
return nil, nil
|
||||
return &TestArtifact{id: tb.artifactId}, nil
|
||||
}
|
||||
|
||||
func (tb *TestBuilder) Cancel() {
|
||||
|
||||
@@ -39,8 +39,21 @@ type RemoteCmd struct {
|
||||
// Communicators must be safe for concurrency, meaning multiple calls to
|
||||
// Start or any other method may be called at the same time.
|
||||
type Communicator interface {
|
||||
// Start takes a RemoteCmd and starts it. The RemoteCmd must not be
|
||||
// modified after being used with Start, and it must not be used with
|
||||
// Start again. The Start method returns immediately once the command
|
||||
// is started. It does not wait for the command to complete. The
|
||||
// RemoteCmd.Exited field should be used for this.
|
||||
Start(*RemoteCmd) error
|
||||
|
||||
// Upload uploads a file to the machine to the given path with the
|
||||
// contents coming from the given reader. This method will block until
|
||||
// it completes.
|
||||
Upload(string, io.Reader) error
|
||||
|
||||
// Download downloads a file from the machine from the given remote path
|
||||
// with the contents writing to the given writer. This method will
|
||||
// block until it completes.
|
||||
Download(string, io.Writer) error
|
||||
}
|
||||
|
||||
|
||||
+28
-4
@@ -19,6 +19,9 @@ type CommandFunc func(name string) (Command, error)
|
||||
// The function type used to lookup Hook implementations.
|
||||
type HookFunc func(name string) (Hook, error)
|
||||
|
||||
// The function type used to lookup PostProcessor implementations.
|
||||
type PostProcessorFunc func(name string) (PostProcessor, error)
|
||||
|
||||
// The function type used to lookup Provisioner implementations.
|
||||
type ProvisionerFunc func(name string) (Provisioner, error)
|
||||
|
||||
@@ -26,10 +29,11 @@ type ProvisionerFunc func(name string) (Provisioner, error)
|
||||
// pointers necessary to look up components of Packer such as builders,
|
||||
// commands, etc.
|
||||
type ComponentFinder struct {
|
||||
Builder BuilderFunc
|
||||
Command CommandFunc
|
||||
Hook HookFunc
|
||||
Provisioner ProvisionerFunc
|
||||
Builder BuilderFunc
|
||||
Command CommandFunc
|
||||
Hook HookFunc
|
||||
PostProcessor PostProcessorFunc
|
||||
Provisioner ProvisionerFunc
|
||||
}
|
||||
|
||||
// The environment interface provides access to the configuration and
|
||||
@@ -42,6 +46,7 @@ type Environment interface {
|
||||
Cache() Cache
|
||||
Cli([]string) (int, error)
|
||||
Hook(string) (Hook, error)
|
||||
PostProcessor(string) (PostProcessor, error)
|
||||
Provisioner(string) (Provisioner, error)
|
||||
Ui() Ui
|
||||
}
|
||||
@@ -104,6 +109,10 @@ func NewEnvironment(config *EnvironmentConfig) (resultEnv Environment, err error
|
||||
env.components.Hook = func(string) (Hook, error) { return nil, nil }
|
||||
}
|
||||
|
||||
if env.components.PostProcessor == nil {
|
||||
env.components.PostProcessor = func(string) (PostProcessor, error) { return nil, nil }
|
||||
}
|
||||
|
||||
if env.components.Provisioner == nil {
|
||||
env.components.Provisioner = func(string) (Provisioner, error) { return nil, nil }
|
||||
}
|
||||
@@ -152,6 +161,21 @@ func (e *coreEnvironment) Hook(name string) (h Hook, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a PostProcessor for the given name that is registered with this
|
||||
// environment.
|
||||
func (e *coreEnvironment) PostProcessor(name string) (p PostProcessor, err error) {
|
||||
p, err = e.components.PostProcessor(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
err = fmt.Errorf("No post processor found for name: %s", name)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a provisioner for the given name that is registered with this
|
||||
// environment.
|
||||
func (e *coreEnvironment) Provisioner(name string) (p Provisioner, err error) {
|
||||
|
||||
@@ -5,11 +5,18 @@ import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Disable log output for tests
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
func testEnvironment() Environment {
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Ui = &ReaderWriterUi{
|
||||
@@ -67,6 +74,7 @@ func TestEnvironment_NilComponents(t *testing.T) {
|
||||
env.Builder("foo")
|
||||
env.Cli([]string{"foo"})
|
||||
env.Hook("foo")
|
||||
env.PostProcessor("foo")
|
||||
env.Provisioner("foo")
|
||||
}
|
||||
|
||||
@@ -246,6 +254,47 @@ func TestEnvironment_Hook_Error(t *testing.T) {
|
||||
assert.Nil(returned, "should be no hook")
|
||||
}
|
||||
|
||||
func TestEnvironment_PostProcessor(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
pp := &TestPostProcessor{}
|
||||
pps := make(map[string]PostProcessor)
|
||||
pps["foo"] = pp
|
||||
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return pps[n], nil }
|
||||
|
||||
env, _ := NewEnvironment(config)
|
||||
returned, err := env.PostProcessor("foo")
|
||||
assert.Nil(err, "should be no error")
|
||||
assert.Equal(returned, pp, "should return correct pp")
|
||||
}
|
||||
|
||||
func TestEnvironment_PostProcessor_NilError(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return nil, nil }
|
||||
|
||||
env, _ := NewEnvironment(config)
|
||||
returned, err := env.PostProcessor("foo")
|
||||
assert.NotNil(err, "should be an error")
|
||||
assert.Nil(returned, "should be no pp")
|
||||
}
|
||||
|
||||
func TestEnvironment_PostProcessor_Error(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Components.PostProcessor = func(n string) (PostProcessor, error) { return nil, errors.New("foo") }
|
||||
|
||||
env, _ := NewEnvironment(config)
|
||||
returned, err := env.PostProcessor("foo")
|
||||
assert.NotNil(err, "should be an error")
|
||||
assert.Equal(err.Error(), "foo", "should be correct error")
|
||||
assert.Nil(returned, "should be no pp")
|
||||
}
|
||||
|
||||
func TestEnvironmentProvisioner(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
|
||||
+8
-4
@@ -12,7 +12,7 @@ const HookProvision = "packer_provision"
|
||||
// in. In addition to that, the Hook is given access to a UI so that it can
|
||||
// output things to the user.
|
||||
type Hook interface {
|
||||
Run(string, Ui, Communicator, interface{})
|
||||
Run(string, Ui, Communicator, interface{}) error
|
||||
}
|
||||
|
||||
// A Hook implementation that dispatches based on an internal mapping.
|
||||
@@ -23,14 +23,18 @@ type DispatchHook struct {
|
||||
// Runs the hook with the given name by dispatching it to the proper
|
||||
// hooks if a mapping exists. If a mapping doesn't exist, then nothing
|
||||
// happens.
|
||||
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
|
||||
func (h *DispatchHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
hooks, ok := h.Mapping[name]
|
||||
if !ok {
|
||||
// No hooks for that name. No problem.
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, hook := range hooks {
|
||||
hook.Run(name, ui, comm, data)
|
||||
if err := hook.Run(name, ui, comm, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-1
@@ -13,12 +13,13 @@ type TestHook struct {
|
||||
runUi Ui
|
||||
}
|
||||
|
||||
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
|
||||
func (t *TestHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
t.runCalled = true
|
||||
t.runComm = comm
|
||||
t.runData = data
|
||||
t.runName = name
|
||||
t.runUi = ui
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDispatchHook_Implements(t *testing.T) {
|
||||
|
||||
@@ -140,6 +140,17 @@ func (c *Client) Hook() (packer.Hook, error) {
|
||||
return &cmdHook{packrpc.Hook(client), c}, nil
|
||||
}
|
||||
|
||||
// Returns a post-processor implementation that is communicating over
|
||||
// this client. If the client hasn't been started, this will start it.
|
||||
func (c *Client) PostProcessor() (packer.PostProcessor, error) {
|
||||
client, err := c.rpcClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmdPostProcessor{packrpc.PostProcessor(client), c}, nil
|
||||
}
|
||||
|
||||
// Returns a provisioner implementation that is communicating over this
|
||||
// client. If the client hasn't been started, this will start it.
|
||||
func (c *Client) Provisioner() (packer.Provisioner, error) {
|
||||
@@ -194,6 +205,7 @@ func (c *Client) Start() (address string, err error) {
|
||||
}
|
||||
|
||||
env := []string{
|
||||
fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
|
||||
fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort),
|
||||
fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ type cmdHook struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) {
|
||||
func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data interface{}) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
c.hook.Run(name, ui, comm, data)
|
||||
return c.hook.Run(name, ui, comm, data)
|
||||
}
|
||||
|
||||
func (c *cmdHook) checkExit(p interface{}, cb func()) {
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
|
||||
type helperHook byte
|
||||
|
||||
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) {}
|
||||
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHook_NoExist(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
|
||||
|
||||
+30
-4
@@ -8,6 +8,7 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
packrpc "github.com/mitchellh/packer/packer/rpc"
|
||||
@@ -21,9 +22,16 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const MagicCookieKey = "PACKER_PLUGIN_MAGIC_COOKIE"
|
||||
const MagicCookieValue = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2"
|
||||
|
||||
// This serves a single RPC connection on the given RPC server on
|
||||
// a random port.
|
||||
func serve(server *rpc.Server) (err error) {
|
||||
if os.Getenv(MagicCookieKey) != MagicCookieValue {
|
||||
return errors.New("Please do not execute plugins directly. Packer will execute these for you.")
|
||||
}
|
||||
|
||||
// If there is no explicit number of Go threads to use, then set it
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
@@ -107,7 +115,8 @@ func ServeBuilder(builder packer.Builder) {
|
||||
|
||||
swallowInterrupts()
|
||||
if err := serve(server); err != nil {
|
||||
log.Panic(err)
|
||||
log.Printf("ERROR: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +129,8 @@ func ServeCommand(command packer.Command) {
|
||||
|
||||
swallowInterrupts()
|
||||
if err := serve(server); err != nil {
|
||||
log.Panic(err)
|
||||
log.Printf("ERROR: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +143,22 @@ func ServeHook(hook packer.Hook) {
|
||||
|
||||
swallowInterrupts()
|
||||
if err := serve(server); err != nil {
|
||||
log.Panic(err)
|
||||
log.Printf("ERROR: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Serves a post-processor from a plugin.
|
||||
func ServePostProcessor(p packer.PostProcessor) {
|
||||
log.Println("Preparing to serve a post-processor plugin...")
|
||||
|
||||
server := rpc.NewServer()
|
||||
packrpc.RegisterPostProcessor(server, p)
|
||||
|
||||
swallowInterrupts()
|
||||
if err := serve(server); err != nil {
|
||||
log.Printf("ERROR: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +171,7 @@ func ServeProvisioner(p packer.Provisioner) {
|
||||
|
||||
swallowInterrupts()
|
||||
if err := serve(server); err != nil {
|
||||
log.Panic(err)
|
||||
log.Printf("ERROR: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ func TestHelperProcess(*testing.T) {
|
||||
case "mock":
|
||||
fmt.Println(":1234")
|
||||
<-make(chan int)
|
||||
case "post-processor":
|
||||
ServePostProcessor(new(helperPostProcessor))
|
||||
case "provisioner":
|
||||
ServeProvisioner(new(helperProvisioner))
|
||||
case "start-timeout":
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type cmdPostProcessor struct {
|
||||
p packer.PostProcessor
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *cmdPostProcessor) Configure(config interface{}) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
return c.p.Configure(config)
|
||||
}
|
||||
|
||||
func (c *cmdPostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Artifact, error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
return c.p.PostProcess(ui, a)
|
||||
}
|
||||
|
||||
func (c *cmdPostProcessor) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type helperPostProcessor byte
|
||||
|
||||
func (helperPostProcessor) Configure(interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helperPostProcessor) PostProcess(packer.Ui, packer.Artifact) (packer.Artifact, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestPostProcessor_NoExist(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
|
||||
defer c.Kill()
|
||||
|
||||
_, err := c.PostProcessor()
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostProcessor_Good(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: helperProcess("post-processor")})
|
||||
defer c.Kill()
|
||||
|
||||
_, err := c.PostProcessor()
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ func (c *cmdProvisioner) Prepare(configs ...interface{}) error {
|
||||
return c.p.Prepare(configs...)
|
||||
}
|
||||
|
||||
func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) {
|
||||
func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
c.p.Provision(ui, comm)
|
||||
return c.p.Provision(ui, comm)
|
||||
}
|
||||
|
||||
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
|
||||
|
||||
@@ -12,7 +12,9 @@ func (helperProvisioner) Prepare(...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) {}
|
||||
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProvisioner_NoExist(t *testing.T) {
|
||||
c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package packer
|
||||
|
||||
// A PostProcessor is responsible for taking an artifact of a build
|
||||
// and doing some sort of post-processing to turn this into another
|
||||
// artifact. An example of a post-processor would be something that takes
|
||||
// the result of a build, compresses it, and returns a new artifact containing
|
||||
// a single file of the prior artifact compressed.
|
||||
type PostProcessor interface {
|
||||
// Configure is responsible for setting up configuration, storing
|
||||
// the state for later, and returning and errors, such as validation
|
||||
// errors.
|
||||
Configure(interface{}) error
|
||||
|
||||
// PostProcess takes a previously created Artifact and produces another
|
||||
// Artifact. If an error occurs, it should return that error.
|
||||
PostProcess(Ui, Artifact) (Artifact, error)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package packer
|
||||
|
||||
type TestPostProcessor struct {
|
||||
artifactId string
|
||||
configCalled bool
|
||||
configVal interface{}
|
||||
ppCalled bool
|
||||
ppArtifact Artifact
|
||||
ppUi Ui
|
||||
}
|
||||
|
||||
func (pp *TestPostProcessor) Configure(v interface{}) error {
|
||||
pp.configCalled = true
|
||||
pp.configVal = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *TestPostProcessor) PostProcess(ui Ui, a Artifact) (Artifact, error) {
|
||||
pp.ppCalled = true
|
||||
pp.ppArtifact = a
|
||||
pp.ppUi = ui
|
||||
return &TestArtifact{id: pp.artifactId}, nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type Provisioner interface {
|
||||
// given to communicate with the user, and a communicator is given that
|
||||
// is guaranteed to be connected to some machine so that provisioning
|
||||
// can be done.
|
||||
Provision(Ui, Communicator)
|
||||
Provision(Ui, Communicator) error
|
||||
}
|
||||
|
||||
// A Hook implementation that runs the given provisioners.
|
||||
@@ -23,8 +23,12 @@ type ProvisionHook struct {
|
||||
}
|
||||
|
||||
// Runs the provisioners in order.
|
||||
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) {
|
||||
func (h *ProvisionHook) Run(name string, ui Ui, comm Communicator, data interface{}) error {
|
||||
for _, p := range h.Provisioners {
|
||||
p.Provision(ui, comm)
|
||||
if err := p.Provision(ui, comm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ func (t *TestProvisioner) Prepare(configs ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestProvisioner) Provision(Ui, Communicator) {
|
||||
func (t *TestProvisioner) Provision(Ui, Communicator) error {
|
||||
t.provCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestProvisionHook_Impl(t *testing.T) {
|
||||
|
||||
@@ -41,6 +41,15 @@ func (a *artifact) String() (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (a *artifact) Destroy() error {
|
||||
var result error
|
||||
if err := a.client.Call("Artifact.Destroy", new(interface{}), &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ArtifactServer) BuilderId(args *interface{}, reply *string) error {
|
||||
*reply = s.artifact.BuilderId()
|
||||
return nil
|
||||
@@ -60,3 +69,13 @@ func (s *ArtifactServer) String(args *interface{}, reply *string) error {
|
||||
*reply = s.artifact.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ArtifactServer) Destroy(args *interface{}, reply *error) error {
|
||||
err := s.artifact.Destroy()
|
||||
if err != nil {
|
||||
err = NewBasicError(err)
|
||||
}
|
||||
|
||||
*reply = err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ func (testArtifact) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (testArtifact) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestArtifactRPC(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user