diff -Nru juju-core-1.17.4/debian/changelog juju-core-1.17.6/debian/changelog --- juju-core-1.17.4/debian/changelog 2014-03-05 09:57:53.000000000 +0000 +++ juju-core-1.17.6/debian/changelog 2014-03-24 16:05:49.000000000 +0000 @@ -1,3 +1,20 @@ +juju-core (1.17.6-0ubuntu1) trusty; urgency=medium + + * New upstream point release, including fixes for: + - br0 not bought up by cloud-init with MAAS provider (LP: #1271144). + - ppc64el enablement for juju/lxc (LP: #1273769). + - juju userdata should not restart networking (LP: #1248283). + - error detecting hardware characteristics (LP: #1276909). + - juju instances not including the default security group (LP: #1129720). + - juju bootstrap does not honor https_proxy (LP: #1240260). + * d/control,rules: Drop BD on bash-completion, install bash-completion + direct from upstream source code. + * d/rules: Set HOME prior to generating man pages. + * d/control: Drop alternative dependency on mongodb-server; juju now only + works on trusty with juju-mongodb. + + -- James Page Mon, 24 Mar 2014 16:05:44 +0000 + juju-core (1.17.4-0ubuntu2) trusty; urgency=medium * d/control: Add rsyslog-gnutls as dependency for juju-local package diff -Nru juju-core-1.17.4/debian/control juju-core-1.17.6/debian/control --- juju-core-1.17.4/debian/control 2014-03-05 09:58:00.000000000 +0000 +++ juju-core-1.17.6/debian/control 2014-03-21 11:49:45.000000000 +0000 @@ -3,7 +3,6 @@ Priority: extra Maintainer: Ubuntu Developers Build-Depends: - bash-completion, debhelper (>= 7.0.50~), gccgo-go [!amd64 !i386 !armhf], golang-go (>= 2:1.1.1) [amd64 i386 armhf], @@ -42,7 +41,7 @@ Architecture: all Depends: juju-core (>= ${source:Version}), - juju-mongodb | mongodb-server, + juju-mongodb, lxc, rsyslog-gnutls, ${misc:Depends} diff -Nru juju-core-1.17.4/debian/rules juju-core-1.17.6/debian/rules --- juju-core-1.17.4/debian/rules 2014-03-05 09:42:48.000000000 +0000 +++ juju-core-1.17.6/debian/rules 2014-03-21 11:48:47.000000000 +0000 @@ -13,7 +13,7 @@ endif %: - dh $@ --with bash-completion + dh $@ debian/juju-core.postinst: debian/juju-core.postinst.in sed -e "s/__NEW_VERSION__/$(VERSION)/g" $< > $@ @@ -38,18 +38,20 @@ go install $(COMMON_FLAGS) launchpad.net/juju-core/cmd/plugins/juju-metadata go install $(COMMON_FLAGS) launchpad.net/juju-core/cmd/plugins/juju-restore go install $(COMMON_FLAGS) $(JUJUD_FLAGS) launchpad.net/juju-core/cmd/jujud - $(GOPATH)/src/launchpad.net/juju-core/scripts/generate-docs.py man -o juju.1 + mkdir -p debian/home + HOME=debian/home $(GOPATH)/src/launchpad.net/juju-core/scripts/generate-docs.py man -o juju.1 dh_install bin/juju usr/lib/juju-$(VERSION)/bin dh_install bin/juju-metadata usr/lib/juju-$(VERSION)/bin dh_install bin/juju-restore usr/lib/juju-$(VERSION)/bin dh_install src/launchpad.net/juju-core/cmd/plugins/juju-backup/juju-backup usr/lib/juju-$(VERSION)/bin dh_install bin/jujud usr/lib/juju-$(VERSION)/bin dh_install juju.1 usr/lib/juju-$(VERSION)/man/man1 + dh_install src/launchpad.net/juju-core/etc/bash_completion.d/juju-core etc/bash_completion.d dh_auto_install override_dh_auto_clean: rm -rf debian/juju-core.prerm debian/juju-core.postinst debian/juju-core.lintian-overrides bin juju.1 - rm -rf pkg bin /tmp/go-build* + rm -rf pkg bin /tmp/go-build* debian/home find . -name "*.pyc" -delete || : dh_auto_clean diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/COPYING juju-core-1.17.6/src/github.com/juju/loggo/COPYING --- juju-core-1.17.4/src/github.com/juju/loggo/COPYING 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/COPYING 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/COPYING.LESSER juju-core-1.17.6/src/github.com/juju/loggo/COPYING.LESSER --- juju-core-1.17.4/src/github.com/juju/loggo/COPYING.LESSER 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/COPYING.LESSER 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/example/first.go juju-core-1.17.6/src/github.com/juju/loggo/example/first.go --- juju-core-1.17.4/src/github.com/juju/loggo/example/first.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/example/first.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/juju/loggo" +) + +var first = loggo.GetLogger("first") + +func FirstCritical(message string) { + first.Criticalf(message) +} + +func FirstError(message string) { + first.Errorf(message) +} + +func FirstWarning(message string) { + first.Warningf(message) +} + +func FirstInfo(message string) { + first.Infof(message) +} + +func FirstTrace(message string) { + first.Tracef(message) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/example/main.go juju-core-1.17.6/src/github.com/juju/loggo/example/main.go --- juju-core-1.17.4/src/github.com/juju/loggo/example/main.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/example/main.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + + "github.com/juju/loggo" +) + +var logger = loggo.GetLogger("main") +var rootLogger = loggo.GetLogger("") + +func main() { + args := os.Args + if len(args) > 1 { + loggo.ConfigureLoggers(args[1]) + } else { + fmt.Println("Add a parameter to configure the logging:") + fmt.Println("E.g. \"=INFO;first=TRACE\"") + } + fmt.Println("\nCurrent logging levels:") + fmt.Println(loggo.LoggerInfo()) + fmt.Println("") + + rootLogger.Infof("Start of test.") + + FirstCritical("first critical") + FirstError("first error") + FirstWarning("first warning") + FirstInfo("first info") + FirstTrace("first trace") + + SecondCritical("first critical") + SecondError("first error") + SecondWarning("first warning") + SecondInfo("first info") + SecondTrace("first trace") + +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/example/second.go juju-core-1.17.6/src/github.com/juju/loggo/example/second.go --- juju-core-1.17.4/src/github.com/juju/loggo/example/second.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/example/second.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/juju/loggo" +) + +var second = loggo.GetLogger("second") + +func SecondCritical(message string) { + second.Criticalf(message) +} + +func SecondError(message string) { + second.Errorf(message) +} + +func SecondWarning(message string) { + second.Warningf(message) +} + +func SecondInfo(message string) { + second.Infof(message) +} + +func SecondTrace(message string) { + second.Tracef(message) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/formatter.go juju-core-1.17.6/src/github.com/juju/loggo/formatter.go --- juju-core-1.17.4/src/github.com/juju/loggo/formatter.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/formatter.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,26 @@ +package loggo + +import ( + "fmt" + "path/filepath" + "time" +) + +// Formatter defines the single method Format, which takes the logging +// information, and converts it to a string. +type Formatter interface { + Format(level Level, module, filename string, line int, timestamp time.Time, message string) string +} + +// DefaultFormatter provides a simple concatenation of all the components. +type DefaultFormatter struct{} + +// Format returns the parameters separated by spaces except for filename and +// line which are separated by a colon. The timestamp is shown to second +// resolution in UTC. +func (*DefaultFormatter) Format(level Level, module, filename string, line int, timestamp time.Time, message string) string { + ts := timestamp.In(time.UTC).Format("2006-01-02 15:04:05") + // Just get the basename from the filename + filename = filepath.Base(filename) + return fmt.Sprintf("%s %s %s %s:%d %s", ts, level, module, filename, line, message) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/formatter_test.go juju-core-1.17.6/src/github.com/juju/loggo/formatter_test.go --- juju-core-1.17.4/src/github.com/juju/loggo/formatter_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/formatter_test.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,22 @@ +package loggo_test + +import ( + "time" + + gc "launchpad.net/gocheck" + + "github.com/juju/loggo" +) + +type formatterSuite struct{} + +var _ = gc.Suite(&formatterSuite{}) + +func (*formatterSuite) TestDefaultFormat(c *gc.C) { + location, err := time.LoadLocation("UTC") + c.Assert(err, gc.IsNil) + testTime := time.Date(2013, 5, 3, 10, 53, 24, 123456, location) + formatter := &loggo.DefaultFormatter{} + formatted := formatter.Format(loggo.WARNING, "test.module", "some/deep/filename", 42, testTime, "hello world!") + c.Assert(formatted, gc.Equals, "2013-05-03 10:53:24 WARNING test.module filename:42 hello world!") +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/.gitignore juju-core-1.17.6/src/github.com/juju/loggo/.gitignore --- juju-core-1.17.4/src/github.com/juju/loggo/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/.gitignore 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1 @@ +example/example diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/LICENSE juju-core-1.17.6/src/github.com/juju/loggo/LICENSE --- juju-core-1.17.4/src/github.com/juju/loggo/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/LICENSE 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,15 @@ +loggo - A logging library for Go + +Copyright 2013, Canonical Ltd. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + +See both COPYING and COPYING.LESSER for the full terms of the GNU Lesser +General Public License. diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/logger.go juju-core-1.17.6/src/github.com/juju/loggo/logger.go --- juju-core-1.17.4/src/github.com/juju/loggo/logger.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/logger.go 2014-03-20 12:54:13.000000000 +0000 @@ -0,0 +1,421 @@ +// Module level logging for Go +// +// This package provides an alternative to the standard library log package. +// +// The actual logging functions never return errors. If you are logging +// something, you really don't want to be worried about the logging +// having trouble. +// +// Modules have names that are defined by dotted strings. +// e.g. "first.second.third" +// +// There is a root module that has the name "". Each module +// (except the root module) has a parent, identified by the part of +// the name without the last dotted value. +// e.g. the parent of "first.second.third" is "first.second" +// the parent of "first.second" is "first" +// the parent of "first" is "" (the root module) +// +// Each module can specify its own severity level. Logging calls that are of +// a lower severity than the module's effective severity level are not written +// out. +// +// Loggers are created using the GetLogger function. +// e.g. logger := loggo.GetLogger("foo.bar") +// +// By default there is one writer registered, which will write to Stderr, +// and the root module, which will only emit warnings and above. +// If you want to continue using the default +// logger, but have it emit all logging levels you need to do the following. +// +// writer, _, err := loggo.RemoveWriter("default") +// // err is non-nil if and only if the name isn't found. +// loggo.RegisterWriter("default", writer, loggo.TRACE) +package loggo + +import ( + "fmt" + "runtime" + "sort" + "strings" + "sync" + "sync/atomic" + "time" +) + +// Level holds a severity level. +type Level uint32 + +// The severity levels. Higher values are more considered more +// important. +const ( + UNSPECIFIED Level = iota + TRACE + DEBUG + INFO + WARNING + ERROR + CRITICAL +) + +// A Logger represents a logging module. It has an associated logging +// level which can be changed; messages of lesser severity will +// be dropped. Loggers have a hierarchical relationship - see +// the package documentation. +// +// The zero Logger value is usable - any messages logged +// to it will be sent to the root Logger. +type Logger struct { + impl *module +} + +type module struct { + name string + level Level + parent *module +} + +// Initially the modules map only contains the root module. +var ( + root = &module{level: WARNING} + modulesMutex sync.Mutex + modules = map[string]*module{ + "": root, + } +) + +func (level Level) String() string { + switch level { + case UNSPECIFIED: + return "UNSPECIFIED" + case TRACE: + return "TRACE" + case DEBUG: + return "DEBUG" + case INFO: + return "INFO" + case WARNING: + return "WARNING" + case ERROR: + return "ERROR" + case CRITICAL: + return "CRITICAL" + } + return "" +} + +// get atomically gets the value of the given level. +func (level *Level) get() Level { + return Level(atomic.LoadUint32((*uint32)(level))) +} + +// set atomically sets the value of the receiver +// to the given level. +func (level *Level) set(newLevel Level) { + atomic.StoreUint32((*uint32)(level), uint32(newLevel)) +} + +// getLoggerInternal assumes that the modulesMutex is locked. +func getLoggerInternal(name string) Logger { + impl, found := modules[name] + if found { + return Logger{impl} + } + parentName := "" + if i := strings.LastIndex(name, "."); i >= 0 { + parentName = name[0:i] + } + parent := getLoggerInternal(parentName).impl + impl = &module{name, UNSPECIFIED, parent} + modules[name] = impl + return Logger{impl} +} + +// GetLogger returns a Logger for the given module name, +// creating it and its parents if necessary. +func GetLogger(name string) Logger { + // Lowercase the module name, and look for it in the modules map. + name = strings.ToLower(name) + modulesMutex.Lock() + defer modulesMutex.Unlock() + return getLoggerInternal(name) +} + +// LoggerInfo returns information about the configured loggers and their logging +// levels. The information is returned in the format expected by +// ConfigureModules. Loggers with UNSPECIFIED level will not +// be included. +func LoggerInfo() string { + output := []string{} + // output in alphabetical order. + keys := []string{} + modulesMutex.Lock() + defer modulesMutex.Unlock() + for key := range modules { + keys = append(keys, key) + } + sort.Strings(keys) + for _, name := range keys { + mod := modules[name] + severity := mod.level + if severity == UNSPECIFIED { + continue + } + output = append(output, fmt.Sprintf("%s=%s", mod.Name(), severity)) + } + return strings.Join(output, ";") +} + +// ParseConfigurationString parses a logger configuration string into a map of +// logger names and their associated log level. This method is provided to +// allow other programs to pre-validate a configuration string rather than +// just calling ConfigureLoggers. +// +// Loggers are colon- or semicolon-separated; each module is specified as +// =. White space outside of module names and levels is +// ignored. The root module is specified with the name "". +// +// An example specification: +// `=ERROR; foo.bar=WARNING` +func ParseConfigurationString(specification string) (map[string]Level, error) { + values := strings.FieldsFunc(specification, func(r rune) bool { return r == ';' || r == ':' }) + levels := make(map[string]Level) + for _, value := range values { + s := strings.SplitN(value, "=", 2) + if len(s) < 2 { + return nil, fmt.Errorf("logger specification expected '=', found %q", value) + } + name := strings.TrimSpace(s[0]) + levelStr := strings.TrimSpace(s[1]) + if name == "" || levelStr == "" { + return nil, fmt.Errorf("logger specification %q has blank name or level", value) + } + if name == "" { + name = "" + } + level, ok := ParseLevel(levelStr) + if !ok { + return nil, fmt.Errorf("unknown severity level %q", levelStr) + } + levels[name] = level + } + return levels, nil +} + +// ConfigureLoggers configures loggers according to the given string +// specification, which specifies a set of modules and their associated +// logging levels. Loggers are colon- or semicolon-separated; each +// module is specified as =. White space outside of +// module names and levels is ignored. The root module is specified +// with the name "". +// +// An example specification: +// `=ERROR; foo.bar=WARNING` +func ConfigureLoggers(specification string) error { + if specification == "" { + return nil + } + levels, err := ParseConfigurationString(specification) + if err != nil { + return err + } + for name, level := range levels { + GetLogger(name).SetLogLevel(level) + } + return nil +} + +// ResetLogging iterates through the known modules and sets the levels of all +// to UNSPECIFIED, except for which is set to WARNING. +func ResetLoggers() { + modulesMutex.Lock() + defer modulesMutex.Unlock() + for name, module := range modules { + if name == "" { + module.level.set(WARNING) + } else { + module.level.set(UNSPECIFIED) + } + } +} + +// ParseLevel converts a string representation of a logging level to a +// Level. It returns the level and whether it was valid or not. +func ParseLevel(level string) (Level, bool) { + level = strings.ToUpper(level) + switch level { + case "UNSPECIFIED": + return UNSPECIFIED, true + case "TRACE": + return TRACE, true + case "DEBUG": + return DEBUG, true + case "INFO": + return INFO, true + case "WARN", "WARNING": + return WARNING, true + case "ERROR": + return ERROR, true + case "CRITICAL": + return CRITICAL, true + } + return UNSPECIFIED, false +} + +func (logger Logger) getModule() *module { + if logger.impl == nil { + return root + } + return logger.impl +} + +// Name returns the logger's module name. +func (logger Logger) Name() string { + return logger.getModule().Name() +} + +// LogLevel returns the configured log level of the logger. +func (logger Logger) LogLevel() Level { + return logger.getModule().level.get() +} + +func (module *module) getEffectiveLogLevel() Level { + // Note: the root module is guaranteed to have a + // specified logging level, so acts as a suitable sentinel + // for this loop. + for { + if level := module.level.get(); level != UNSPECIFIED { + return level + } + module = module.parent + } + panic("unreachable") +} + +func (module *module) Name() string { + if module.name == "" { + return "" + } + return module.name +} + +// EffectiveLogLevel returns the effective log level of +// the receiver - that is, messages with a lesser severity +// level will be discarded. +// +// If the log level of the receiver is unspecified, +// it will be taken from the effective log level of its +// parent. +func (logger Logger) EffectiveLogLevel() Level { + return logger.getModule().getEffectiveLogLevel() +} + +// SetLogLevel sets the severity level of the given logger. +// The root logger cannot be set to UNSPECIFIED level. +// See EffectiveLogLevel for how this affects the +// actual messages logged. +func (logger Logger) SetLogLevel(level Level) { + module := logger.getModule() + // The root module can't be unspecified. + if module.name == "" && level == UNSPECIFIED { + level = WARNING + } + module.level.set(level) +} + +// Logf logs a printf-formatted message at the given level. +// A message will be discarded if level is less than the +// the effective log level of the logger. +// Note that the writers may also filter out messages that +// are less than their registered minimum severity level. +func (logger Logger) Logf(level Level, message string, args ...interface{}) { + if logger.getModule().getEffectiveLogLevel() > level || + !WillWrite(level) || + level < TRACE || + level > CRITICAL { + return + } + // Gather time, and filename, line number. + now := time.Now() // get this early. + // Param to Caller is the call depth. Since this method is called from + // the Logger methods, we want the place that those were called from. + _, file, line, ok := runtime.Caller(2) + if !ok { + file = "???" + line = 0 + } + // Trim newline off format string, following usual + // Go logging conventions. + if len(message) > 0 && message[len(message)-1] == '\n' { + message = message[0 : len(message)-1] + } + + formattedMessage := fmt.Sprintf(message, args...) + writeToWriters(level, logger.impl.name, file, line, now, formattedMessage) +} + +// Criticalf logs the printf-formatted message at critical level. +func (logger Logger) Criticalf(message string, args ...interface{}) { + logger.Logf(CRITICAL, message, args...) +} + +// Errorf logs the printf-formatted message at error level. +func (logger Logger) Errorf(message string, args ...interface{}) { + logger.Logf(ERROR, message, args...) +} + +// Warningf logs the printf-formatted message at warning level. +func (logger Logger) Warningf(message string, args ...interface{}) { + logger.Logf(WARNING, message, args...) +} + +// Infof logs the printf-formatted message at info level. +func (logger Logger) Infof(message string, args ...interface{}) { + logger.Logf(INFO, message, args...) +} + +// Debugf logs the printf-formatted message at debug level. +func (logger Logger) Debugf(message string, args ...interface{}) { + logger.Logf(DEBUG, message, args...) +} + +// Tracef logs the printf-formatted message at trace level. +func (logger Logger) Tracef(message string, args ...interface{}) { + logger.Logf(TRACE, message, args...) +} + +// IsLevelEnabled returns whether debugging is enabled +// for the given log level. +func (logger Logger) IsLevelEnabled(level Level) bool { + return logger.getModule().getEffectiveLogLevel() <= level +} + +// IsErrorEnabled returns whether debugging is enabled +// at error level. +func (logger Logger) IsErrorEnabled() bool { + return logger.IsLevelEnabled(ERROR) +} + +// IsWarningEnabled returns whether debugging is enabled +// at warning level. +func (logger Logger) IsWarningEnabled() bool { + return logger.IsLevelEnabled(WARNING) +} + +// IsInfoEnabled returns whether debugging is enabled +// at info level. +func (logger Logger) IsInfoEnabled() bool { + return logger.IsLevelEnabled(INFO) +} + +// IsDebugEnabled returns whether debugging is enabled +// at debug level. +func (logger Logger) IsDebugEnabled() bool { + return logger.IsLevelEnabled(DEBUG) +} + +// IsTraceEnabled returns whether debugging is enabled +// at trace level. +func (logger Logger) IsTraceEnabled() bool { + return logger.IsLevelEnabled(TRACE) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/logger_test.go juju-core-1.17.6/src/github.com/juju/loggo/logger_test.go --- juju-core-1.17.4/src/github.com/juju/loggo/logger_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/logger_test.go 2014-03-20 12:54:13.000000000 +0000 @@ -0,0 +1,436 @@ +package loggo_test + +import ( + "io/ioutil" + "os" + "testing" + + gc "launchpad.net/gocheck" + + "github.com/juju/loggo" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} + +type loggerSuite struct{} + +var _ = gc.Suite(&loggerSuite{}) + +func (*loggerSuite) SetUpTest(c *gc.C) { + loggo.ResetLoggers() +} + +func (*loggerSuite) TestRootLogger(c *gc.C) { + root := loggo.Logger{} + c.Assert(root.Name(), gc.Equals, "") + c.Assert(root.IsErrorEnabled(), gc.Equals, true) + c.Assert(root.IsWarningEnabled(), gc.Equals, true) + c.Assert(root.IsInfoEnabled(), gc.Equals, false) + c.Assert(root.IsDebugEnabled(), gc.Equals, false) + c.Assert(root.IsTraceEnabled(), gc.Equals, false) +} + +func (*loggerSuite) TestModuleName(c *gc.C) { + logger := loggo.GetLogger("loggo.testing") + c.Assert(logger.Name(), gc.Equals, "loggo.testing") +} + +func (*loggerSuite) TestSetLevel(c *gc.C) { + logger := loggo.GetLogger("testing") + + c.Assert(logger.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, true) + c.Assert(logger.IsInfoEnabled(), gc.Equals, false) + c.Assert(logger.IsDebugEnabled(), gc.Equals, false) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + logger.SetLogLevel(loggo.TRACE) + c.Assert(logger.LogLevel(), gc.Equals, loggo.TRACE) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.TRACE) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, true) + c.Assert(logger.IsInfoEnabled(), gc.Equals, true) + c.Assert(logger.IsDebugEnabled(), gc.Equals, true) + c.Assert(logger.IsTraceEnabled(), gc.Equals, true) + logger.SetLogLevel(loggo.DEBUG) + c.Assert(logger.LogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, true) + c.Assert(logger.IsInfoEnabled(), gc.Equals, true) + c.Assert(logger.IsDebugEnabled(), gc.Equals, true) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + logger.SetLogLevel(loggo.INFO) + c.Assert(logger.LogLevel(), gc.Equals, loggo.INFO) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.INFO) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, true) + c.Assert(logger.IsInfoEnabled(), gc.Equals, true) + c.Assert(logger.IsDebugEnabled(), gc.Equals, false) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + logger.SetLogLevel(loggo.WARNING) + c.Assert(logger.LogLevel(), gc.Equals, loggo.WARNING) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, true) + c.Assert(logger.IsInfoEnabled(), gc.Equals, false) + c.Assert(logger.IsDebugEnabled(), gc.Equals, false) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + logger.SetLogLevel(loggo.ERROR) + c.Assert(logger.LogLevel(), gc.Equals, loggo.ERROR) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(logger.IsErrorEnabled(), gc.Equals, true) + c.Assert(logger.IsWarningEnabled(), gc.Equals, false) + c.Assert(logger.IsInfoEnabled(), gc.Equals, false) + c.Assert(logger.IsDebugEnabled(), gc.Equals, false) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + // This is added for completeness, but not really expected to be used. + logger.SetLogLevel(loggo.CRITICAL) + c.Assert(logger.LogLevel(), gc.Equals, loggo.CRITICAL) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.CRITICAL) + c.Assert(logger.IsErrorEnabled(), gc.Equals, false) + c.Assert(logger.IsWarningEnabled(), gc.Equals, false) + c.Assert(logger.IsInfoEnabled(), gc.Equals, false) + c.Assert(logger.IsDebugEnabled(), gc.Equals, false) + c.Assert(logger.IsTraceEnabled(), gc.Equals, false) + logger.SetLogLevel(loggo.UNSPECIFIED) + c.Assert(logger.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING) +} + +func (*loggerSuite) TestLevelsSharedForSameModule(c *gc.C) { + logger1 := loggo.GetLogger("testing.module") + logger2 := loggo.GetLogger("testing.module") + + logger1.SetLogLevel(loggo.INFO) + c.Assert(logger1.IsInfoEnabled(), gc.Equals, true) + c.Assert(logger2.IsInfoEnabled(), gc.Equals, true) +} + +func (*loggerSuite) TestModuleLowered(c *gc.C) { + logger1 := loggo.GetLogger("TESTING.MODULE") + logger2 := loggo.GetLogger("Testing") + + c.Assert(logger1.Name(), gc.Equals, "testing.module") + c.Assert(logger2.Name(), gc.Equals, "testing") +} + +func (*loggerSuite) TestLevelsInherited(c *gc.C) { + root := loggo.GetLogger("") + first := loggo.GetLogger("first") + second := loggo.GetLogger("first.second") + + root.SetLogLevel(loggo.ERROR) + c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR) + c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(first.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(second.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + + first.SetLogLevel(loggo.DEBUG) + c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR) + c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(first.LogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(second.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.DEBUG) + + second.SetLogLevel(loggo.INFO) + c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR) + c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(first.LogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.DEBUG) + c.Assert(second.LogLevel(), gc.Equals, loggo.INFO) + c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.INFO) + + first.SetLogLevel(loggo.UNSPECIFIED) + c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR) + c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(first.LogLevel(), gc.Equals, loggo.UNSPECIFIED) + c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.ERROR) + c.Assert(second.LogLevel(), gc.Equals, loggo.INFO) + c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.INFO) +} + +var parseLevelTests = []struct { + str string + level loggo.Level + fail bool +}{{ + str: "trace", + level: loggo.TRACE, +}, { + str: "TrAce", + level: loggo.TRACE, +}, { + str: "TRACE", + level: loggo.TRACE, +}, { + str: "debug", + level: loggo.DEBUG, +}, { + str: "DEBUG", + level: loggo.DEBUG, +}, { + str: "info", + level: loggo.INFO, +}, { + str: "INFO", + level: loggo.INFO, +}, { + str: "warn", + level: loggo.WARNING, +}, { + str: "WARN", + level: loggo.WARNING, +}, { + str: "warning", + level: loggo.WARNING, +}, { + str: "WARNING", + level: loggo.WARNING, +}, { + str: "error", + level: loggo.ERROR, +}, { + str: "ERROR", + level: loggo.ERROR, +}, { + str: "critical", + level: loggo.CRITICAL, +}, { + str: "not_specified", + fail: true, +}, { + str: "other", + fail: true, +}, { + str: "", + fail: true, +}} + +func (*loggerSuite) TestParseLevel(c *gc.C) { + for _, test := range parseLevelTests { + level, ok := loggo.ParseLevel(test.str) + c.Assert(level, gc.Equals, test.level) + c.Assert(ok, gc.Equals, !test.fail) + } +} + +var levelStringValueTests = map[loggo.Level]string{ + loggo.UNSPECIFIED: "UNSPECIFIED", + loggo.DEBUG: "DEBUG", + loggo.TRACE: "TRACE", + loggo.INFO: "INFO", + loggo.WARNING: "WARNING", + loggo.ERROR: "ERROR", + loggo.CRITICAL: "CRITICAL", + loggo.Level(42): "", // other values are unknown +} + +func (*loggerSuite) TestLevelStringValue(c *gc.C) { + for level, str := range levelStringValueTests { + c.Assert(level.String(), gc.Equals, str) + } +} + +var configureLoggersTests = []struct { + spec string + info string + err string +}{{ + spec: "", + info: "=WARNING", +}, { + spec: "=UNSPECIFIED", + info: "=WARNING", +}, { + spec: "=DEBUG", + info: "=DEBUG", +}, { + spec: "test.module=debug", + info: "=WARNING;test.module=DEBUG", +}, { + spec: "module=info; sub.module=debug; other.module=warning", + info: "=WARNING;module=INFO;other.module=WARNING;sub.module=DEBUG", +}, { + spec: " foo.bar \t\r\n= \t\r\nCRITICAL \t\r\n; \t\r\nfoo \r\t\n = DEBUG", + info: "=WARNING;foo=DEBUG;foo.bar=CRITICAL", +}, { + spec: "foo;bar", + info: "=WARNING", + err: `logger specification expected '=', found "foo"`, +}, { + spec: "=foo", + info: "=WARNING", + err: `logger specification "=foo" has blank name or level`, +}, { + spec: "foo=", + info: "=WARNING", + err: `logger specification "foo=" has blank name or level`, +}, { + spec: "=", + info: "=WARNING", + err: `logger specification "=" has blank name or level`, +}, { + spec: "foo=unknown", + info: "=WARNING", + err: `unknown severity level "unknown"`, +}, { + // Test that nothing is changed even when the + // first part of the specification parses ok. + spec: "module=info; foo=unknown", + info: "=WARNING", + err: `unknown severity level "unknown"`, +}} + +func (*loggerSuite) TestConfigureLoggers(c *gc.C) { + for i, test := range configureLoggersTests { + c.Logf("test %d: %q", i, test.spec) + loggo.ResetLoggers() + err := loggo.ConfigureLoggers(test.spec) + c.Check(loggo.LoggerInfo(), gc.Equals, test.info) + if test.err != "" { + c.Assert(err, gc.ErrorMatches, test.err) + continue + } + c.Assert(err, gc.IsNil) + + // Test that it's idempotent. + err = loggo.ConfigureLoggers(test.spec) + c.Assert(err, gc.IsNil) + c.Assert(loggo.LoggerInfo(), gc.Equals, test.info) + + // Test that calling ConfigureLoggers with the + // output of LoggerInfo works too. + err = loggo.ConfigureLoggers(test.info) + c.Assert(err, gc.IsNil) + c.Assert(loggo.LoggerInfo(), gc.Equals, test.info) + } +} + +type logwriterSuite struct { + logger loggo.Logger + writer *loggo.TestWriter +} + +var _ = gc.Suite(&logwriterSuite{}) + +func (s *logwriterSuite) SetUpTest(c *gc.C) { + loggo.ResetLoggers() + loggo.RemoveWriter("default") + s.writer = &loggo.TestWriter{} + err := loggo.RegisterWriter("test", s.writer, loggo.TRACE) + c.Assert(err, gc.IsNil) + s.logger = loggo.GetLogger("test.writer") + // Make it so the logger itself writes all messages. + s.logger.SetLogLevel(loggo.TRACE) +} + +func (s *logwriterSuite) TearDownTest(c *gc.C) { + loggo.ResetWriters() +} + +func (s *logwriterSuite) TestLogDoesntLogWeirdLevels(c *gc.C) { + s.logger.Logf(loggo.UNSPECIFIED, "message") + c.Assert(s.writer.Log, gc.HasLen, 0) + + s.logger.Logf(loggo.Level(42), "message") + c.Assert(s.writer.Log, gc.HasLen, 0) + + s.logger.Logf(loggo.CRITICAL+loggo.Level(1), "message") + c.Assert(s.writer.Log, gc.HasLen, 0) +} + +func (s *logwriterSuite) TestMessageFormatting(c *gc.C) { + s.logger.Logf(loggo.INFO, "some %s included", "formatting") + c.Assert(s.writer.Log, gc.HasLen, 1) + c.Assert(s.writer.Log[0].Message, gc.Equals, "some formatting included") + c.Assert(s.writer.Log[0].Level, gc.Equals, loggo.INFO) +} + +func (s *logwriterSuite) BenchmarkLoggingNoWriters(c *gc.C) { + // No writers + loggo.RemoveWriter("test") + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning for %d", i) + } +} + +func (s *logwriterSuite) BenchmarkLoggingNoWritersNoFormat(c *gc.C) { + // No writers + loggo.RemoveWriter("test") + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning") + } +} + +func (s *logwriterSuite) BenchmarkLoggingTestWriters(c *gc.C) { + for i := 0; i < c.N; i++ { + s.logger.Warningf("just a simple warning for %d", i) + } + c.Assert(s.writer.Log, gc.HasLen, c.N) +} + +func setupTempFileWriter(c *gc.C) (logFile *os.File, cleanup func()) { + loggo.RemoveWriter("test") + logFile, err := ioutil.TempFile("", "loggo-test") + c.Assert(err, gc.IsNil) + cleanup = func() { + logFile.Close() + os.Remove(logFile.Name()) + } + writer := loggo.NewSimpleWriter(logFile, &loggo.DefaultFormatter{}) + err = loggo.RegisterWriter("testfile", writer, loggo.TRACE) + c.Assert(err, gc.IsNil) + return +} + +func (s *logwriterSuite) BenchmarkLoggingDiskWriter(c *gc.C) { + logFile, cleanup := setupTempFileWriter(c) + defer cleanup() + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Warningf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert((offset > int64(len(msg))*int64(c.N)), gc.Equals, true, + gc.Commentf("Not enough data was written to the log file.")) +} + +func (s *logwriterSuite) BenchmarkLoggingDiskWriterNoMessages(c *gc.C) { + logFile, cleanup := setupTempFileWriter(c) + defer cleanup() + // Change the log level + writer, _, err := loggo.RemoveWriter("testfile") + c.Assert(err, gc.IsNil) + loggo.RegisterWriter("testfile", writer, loggo.WARNING) + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Debugf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert(offset, gc.Equals, int64(0), + gc.Commentf("Data was written to the log file.")) +} + +func (s *logwriterSuite) BenchmarkLoggingDiskWriterNoMessagesLogLevel(c *gc.C) { + logFile, cleanup := setupTempFileWriter(c) + defer cleanup() + // Change the log level + s.logger.SetLogLevel(loggo.WARNING) + msg := "just a simple warning for %d" + for i := 0; i < c.N; i++ { + s.logger.Debugf(msg, i) + } + offset, err := logFile.Seek(0, os.SEEK_CUR) + c.Assert(err, gc.IsNil) + c.Assert(offset, gc.Equals, int64(0), + gc.Commentf("Data was written to the log file.")) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/README.md juju-core-1.17.6/src/github.com/juju/loggo/README.md --- juju-core-1.17.4/src/github.com/juju/loggo/README.md 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/README.md 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,46 @@ +loggo - hierarchical loggers for Go +=================================== + +This package provides an alternative to the standard library log package. + +The actual logging functions never return errors. If you are logging +something, you really don't want to be worried about the logging +having trouble. + +Modules have names that are defined by dotted strings. +``` +"first.second.third" +``` + +There is a root module that has the name `""`. Each module +(except the root module) has a parent, identified by the part of +the name without the last dotted value. +* the parent of `"first.second.third"` is `"first.second"` +* the parent of `"first.second"` is `"first"` +* the parent of `"first"` is `""` (the root module) + +Each module can specify its own severity level. Logging calls that are of +a lower severity than the module's effective severity level are not written +out. + +Loggers are created using the GetLogger function. +``` +logger := loggo.GetLogger("foo.bar") +``` + +By default there is one writer registered, which will write to Stderr, +and the root module, which will only emit warnings and above. + +Use the usual `Sprintf` syntax for: +``` +logger.Criticalf(...) +logger.Errorf(...) +logger.Warningf(...) +logger.Infof(...) +logger.Debugf(...) +logger.Tracef(...) +``` + +If in doubt, look at the tests. They are quite good at observing the expected behaviour. + +The loggers themselves are go-routine safe. The locking is on the output to the writers, and transparent to the caller. diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/testwriter.go juju-core-1.17.6/src/github.com/juju/loggo/testwriter.go --- juju-core-1.17.4/src/github.com/juju/loggo/testwriter.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/testwriter.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,36 @@ +package loggo + +import ( + "path" + "time" +) + +// TestLogValues represents a single logging call. +type TestLogValues struct { + Level Level + Module string + Filename string + Line int + Timestamp time.Time + Message string +} + +// TestWriter is a useful Writer for testing purposes. Each component of the +// logging message is stored in the Log array. +type TestWriter struct { + Log []TestLogValues +} + +// Write saves the params as members in the TestLogValues struct appended to the Log array. +func (writer *TestWriter) Write(level Level, module, filename string, line int, timestamp time.Time, message string) { + if writer.Log == nil { + writer.Log = []TestLogValues{} + } + writer.Log = append(writer.Log, + TestLogValues{level, module, path.Base(filename), line, timestamp, message}) +} + +// Clear removes any saved log messages. +func (writer *TestWriter) Clear() { + writer.Log = []TestLogValues{} +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/writer.go juju-core-1.17.6/src/github.com/juju/loggo/writer.go --- juju-core-1.17.4/src/github.com/juju/loggo/writer.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/writer.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,149 @@ +package loggo + +import ( + "fmt" + "io" + "os" + "sync" + "time" +) + +// Writer is implemented by any recipient of log messages. +type Writer interface { + // Write writes a message to the Writer with the given + // level and module name. The filename and line hold + // the file name and line number of the code that is + // generating the log message; the time stamp holds + // the time the log message was generated, and + // message holds the log message itself. + Write(level Level, name, filename string, line int, timestamp time.Time, message string) +} + +type registeredWriter struct { + writer Writer + level Level +} + +// defaultName is the name of a writer that is registered +// by default that writes to stderr. +const defaultName = "default" + +var ( + writerMutex sync.Mutex + writers = map[string]*registeredWriter{ + defaultName: ®isteredWriter{ + writer: NewSimpleWriter(os.Stderr, &DefaultFormatter{}), + level: TRACE, + }, + } + globalMinLevel = TRACE +) + +// ResetWriters puts the list of writers back into the initial state. +func ResetWriters() { + writerMutex.Lock() + defer writerMutex.Unlock() + writers = map[string]*registeredWriter{ + "default": ®isteredWriter{ + writer: NewSimpleWriter(os.Stderr, &DefaultFormatter{}), + level: TRACE, + }, + } + findMinLevel() +} + +// ReplaceDefaultWriter is a convenience method that does the equivalent of +// RemoveWriter and then RegisterWriter with the name "default". The previous +// default writer, if any is returned. +func ReplaceDefaultWriter(writer Writer) (Writer, error) { + if writer == nil { + return nil, fmt.Errorf("Writer cannot be nil") + } + writerMutex.Lock() + defer writerMutex.Unlock() + reg, found := writers[defaultName] + if !found { + return nil, fmt.Errorf("there is no %q writer", defaultName) + } + oldWriter := reg.writer + reg.writer = writer + return oldWriter, nil + +} + +// RegisterWriter adds the writer to the list of writers that get notified +// when logging. When registering, the caller specifies the minimum logging +// level that will be written, and a name for the writer. If there is already +// a registered writer with that name, an error is returned. +func RegisterWriter(name string, writer Writer, minLevel Level) error { + if writer == nil { + return fmt.Errorf("Writer cannot be nil") + } + writerMutex.Lock() + defer writerMutex.Unlock() + if _, found := writers[name]; found { + return fmt.Errorf("there is already a Writer registered with the name %q", name) + } + writers[name] = ®isteredWriter{writer: writer, level: minLevel} + findMinLevel() + return nil +} + +// RemoveWriter removes the Writer identified by 'name' and returns it. +// If the Writer is not found, an error is returned. +func RemoveWriter(name string) (Writer, Level, error) { + writerMutex.Lock() + defer writerMutex.Unlock() + registered, found := writers[name] + if !found { + return nil, UNSPECIFIED, fmt.Errorf("Writer %q is not registered", name) + } + delete(writers, name) + findMinLevel() + return registered.writer, registered.level, nil +} + +func findMinLevel() { + // We assume the lock is already held + minLevel := CRITICAL + for _, registered := range writers { + if registered.level < minLevel { + minLevel = registered.level + } + } + globalMinLevel.set(minLevel) +} + +// WillWrite returns whether there are any writers registered +// at or above the given severity level. If it returns +// false, a log message at the given level will be discarded. +func WillWrite(level Level) bool { + return level >= globalMinLevel.get() +} + +func writeToWriters(level Level, module, filename string, line int, timestamp time.Time, message string) { + writerMutex.Lock() + defer writerMutex.Unlock() + for _, registered := range writers { + if level >= registered.level { + registered.writer.Write(level, module, filename, line, timestamp, message) + } + } +} + +type simpleWriter struct { + writer io.Writer + formatter Formatter +} + +// NewSimpleWriter returns a new writer that writes +// log messages to the given io.Writer formatting the +// messages with the given formatter. +func NewSimpleWriter(writer io.Writer, formatter Formatter) Writer { + return &simpleWriter{writer, formatter} +} + +func (simple *simpleWriter) Write(level Level, module, filename string, line int, timestamp time.Time, message string) { + logLine := simple.formatter.Format(level, module, filename, line, timestamp, message) + fmt.Fprintln(simple.writer, logLine) +} diff -Nru juju-core-1.17.4/src/github.com/juju/loggo/writer_test.go juju-core-1.17.6/src/github.com/juju/loggo/writer_test.go --- juju-core-1.17.4/src/github.com/juju/loggo/writer_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/loggo/writer_test.go 2014-03-20 12:52:46.000000000 +0000 @@ -0,0 +1,242 @@ +package loggo_test + +import ( + "fmt" + "time" + + gc "launchpad.net/gocheck" + + "github.com/juju/loggo" +) + +type writerBasicsSuite struct{} + +var _ = gc.Suite(&writerBasicsSuite{}) + +func (s *writerBasicsSuite) TearDownTest(c *gc.C) { + loggo.ResetWriters() +} + +func (*writerBasicsSuite) TestRemoveDefaultWriter(c *gc.C) { + defaultWriter, level, err := loggo.RemoveWriter("default") + c.Assert(err, gc.IsNil) + c.Assert(level, gc.Equals, loggo.TRACE) + c.Assert(defaultWriter, gc.NotNil) + + // Trying again fails. + defaultWriter, level, err = loggo.RemoveWriter("default") + c.Assert(err, gc.ErrorMatches, `Writer "default" is not registered`) + c.Assert(level, gc.Equals, loggo.UNSPECIFIED) + c.Assert(defaultWriter, gc.IsNil) +} + +func (*writerBasicsSuite) TestRegisterWriterExistingName(c *gc.C) { + err := loggo.RegisterWriter("default", &loggo.TestWriter{}, loggo.INFO) + c.Assert(err, gc.ErrorMatches, `there is already a Writer registered with the name "default"`) +} + +func (*writerBasicsSuite) TestRegisterNilWriter(c *gc.C) { + err := loggo.RegisterWriter("nil", nil, loggo.INFO) + c.Assert(err, gc.ErrorMatches, `Writer cannot be nil`) +} + +func (*writerBasicsSuite) TestRegisterWriterTypedNil(c *gc.C) { + // If the interface is a typed nil, we have to trust the user. + var writer *loggo.TestWriter + err := loggo.RegisterWriter("nil", writer, loggo.INFO) + c.Assert(err, gc.IsNil) +} + +func (*writerBasicsSuite) TestReplaceDefaultWriter(c *gc.C) { + oldWriter, err := loggo.ReplaceDefaultWriter(&loggo.TestWriter{}) + c.Assert(oldWriter, gc.NotNil) + c.Assert(err, gc.IsNil) +} + +func (*writerBasicsSuite) TestReplaceDefaultWriterWithNil(c *gc.C) { + oldWriter, err := loggo.ReplaceDefaultWriter(nil) + c.Assert(oldWriter, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "Writer cannot be nil") +} + +func (*writerBasicsSuite) TestReplaceDefaultWriterNoDefault(c *gc.C) { + loggo.RemoveWriter("default") + oldWriter, err := loggo.ReplaceDefaultWriter(&loggo.TestWriter{}) + c.Assert(oldWriter, gc.IsNil) + c.Assert(err, gc.ErrorMatches, `there is no "default" writer`) +} + +func (s *writerBasicsSuite) TestWillWrite(c *gc.C) { + // By default, the root logger watches TRACE messages + c.Assert(loggo.WillWrite(loggo.TRACE), gc.Equals, true) + // Note: ReplaceDefaultWriter doesn't let us change the default log + // level :( + writer, _, err := loggo.RemoveWriter("default") + c.Assert(err, gc.IsNil) + c.Assert(writer, gc.NotNil) + err = loggo.RegisterWriter("default", writer, loggo.CRITICAL) + c.Assert(err, gc.IsNil) + c.Assert(loggo.WillWrite(loggo.TRACE), gc.Equals, false) + c.Assert(loggo.WillWrite(loggo.DEBUG), gc.Equals, false) + c.Assert(loggo.WillWrite(loggo.INFO), gc.Equals, false) + c.Assert(loggo.WillWrite(loggo.WARNING), gc.Equals, false) + c.Assert(loggo.WillWrite(loggo.CRITICAL), gc.Equals, true) +} + +type writerSuite struct { + logger loggo.Logger +} + +var _ = gc.Suite(&writerSuite{}) + +func (s *writerSuite) SetUpTest(c *gc.C) { + loggo.ResetLoggers() + loggo.RemoveWriter("default") + s.logger = loggo.GetLogger("test.writer") + // Make it so the logger itself writes all messages. + s.logger.SetLogLevel(loggo.TRACE) +} + +func (s *writerSuite) TearDownTest(c *gc.C) { + loggo.ResetWriters() +} + +func (s *writerSuite) TearDownSuite(c *gc.C) { + loggo.ResetLoggers() +} + +func (s *writerSuite) TestWritingCapturesFileAndLineAndModule(c *gc.C) { + writer := &loggo.TestWriter{} + err := loggo.RegisterWriter("test", writer, loggo.INFO) + c.Assert(err, gc.IsNil) + + s.logger.Infof("Info message") + + // WARNING: test checks the line number of the above logger lines, this + // will mean that if the above line moves, the test will fail unless + // updated. + c.Assert(writer.Log, gc.HasLen, 1) + c.Assert(writer.Log[0].Filename, gc.Equals, "writer_test.go") + c.Assert(writer.Log[0].Line, gc.Equals, 113) + c.Assert(writer.Log[0].Module, gc.Equals, "test.writer") +} + +func (s *writerSuite) TestWritingLimitWarning(c *gc.C) { + writer := &loggo.TestWriter{} + err := loggo.RegisterWriter("test", writer, loggo.WARNING) + c.Assert(err, gc.IsNil) + + start := time.Now() + s.logger.Criticalf("Something critical.") + s.logger.Errorf("An error.") + s.logger.Warningf("A warning message") + s.logger.Infof("Info message") + s.logger.Tracef("Trace the function") + end := time.Now() + + c.Assert(writer.Log, gc.HasLen, 3) + c.Assert(writer.Log[0].Level, gc.Equals, loggo.CRITICAL) + c.Assert(writer.Log[0].Message, gc.Equals, "Something critical.") + c.Assert(writer.Log[0].Timestamp, Between(start, end)) + + c.Assert(writer.Log[1].Level, gc.Equals, loggo.ERROR) + c.Assert(writer.Log[1].Message, gc.Equals, "An error.") + c.Assert(writer.Log[1].Timestamp, Between(start, end)) + + c.Assert(writer.Log[2].Level, gc.Equals, loggo.WARNING) + c.Assert(writer.Log[2].Message, gc.Equals, "A warning message") + c.Assert(writer.Log[2].Timestamp, Between(start, end)) +} + +func (s *writerSuite) TestWritingLimitTrace(c *gc.C) { + writer := &loggo.TestWriter{} + err := loggo.RegisterWriter("test", writer, loggo.TRACE) + c.Assert(err, gc.IsNil) + + start := time.Now() + s.logger.Criticalf("Something critical.") + s.logger.Errorf("An error.") + s.logger.Warningf("A warning message") + s.logger.Infof("Info message") + s.logger.Tracef("Trace the function") + end := time.Now() + + c.Assert(writer.Log, gc.HasLen, 5) + c.Assert(writer.Log[0].Level, gc.Equals, loggo.CRITICAL) + c.Assert(writer.Log[0].Message, gc.Equals, "Something critical.") + c.Assert(writer.Log[0].Timestamp, Between(start, end)) + + c.Assert(writer.Log[1].Level, gc.Equals, loggo.ERROR) + c.Assert(writer.Log[1].Message, gc.Equals, "An error.") + c.Assert(writer.Log[1].Timestamp, Between(start, end)) + + c.Assert(writer.Log[2].Level, gc.Equals, loggo.WARNING) + c.Assert(writer.Log[2].Message, gc.Equals, "A warning message") + c.Assert(writer.Log[2].Timestamp, Between(start, end)) + + c.Assert(writer.Log[3].Level, gc.Equals, loggo.INFO) + c.Assert(writer.Log[3].Message, gc.Equals, "Info message") + c.Assert(writer.Log[3].Timestamp, Between(start, end)) + + c.Assert(writer.Log[4].Level, gc.Equals, loggo.TRACE) + c.Assert(writer.Log[4].Message, gc.Equals, "Trace the function") + c.Assert(writer.Log[4].Timestamp, Between(start, end)) +} + +func (s *writerSuite) TestMultipleWriters(c *gc.C) { + errorWriter := &loggo.TestWriter{} + err := loggo.RegisterWriter("error", errorWriter, loggo.ERROR) + c.Assert(err, gc.IsNil) + warningWriter := &loggo.TestWriter{} + err = loggo.RegisterWriter("warning", warningWriter, loggo.WARNING) + c.Assert(err, gc.IsNil) + infoWriter := &loggo.TestWriter{} + err = loggo.RegisterWriter("info", infoWriter, loggo.INFO) + c.Assert(err, gc.IsNil) + traceWriter := &loggo.TestWriter{} + err = loggo.RegisterWriter("trace", traceWriter, loggo.TRACE) + c.Assert(err, gc.IsNil) + + s.logger.Errorf("An error.") + s.logger.Warningf("A warning message") + s.logger.Infof("Info message") + s.logger.Tracef("Trace the function") + + c.Assert(errorWriter.Log, gc.HasLen, 1) + c.Assert(warningWriter.Log, gc.HasLen, 2) + c.Assert(infoWriter.Log, gc.HasLen, 3) + c.Assert(traceWriter.Log, gc.HasLen, 4) +} + +func Between(start, end time.Time) gc.Checker { + if end.Before(start) { + return &betweenChecker{end, start} + } + return &betweenChecker{start, end} +} + +type betweenChecker struct { + start, end time.Time +} + +func (checker *betweenChecker) Info() *gc.CheckerInfo { + info := gc.CheckerInfo{ + Name: "Between", + Params: []string{"obtained"}, + } + return &info +} + +func (checker *betweenChecker) Check(params []interface{}, names []string) (result bool, error string) { + when, ok := params[0].(time.Time) + if !ok { + return false, "obtained value type must be time.Time" + } + if when.Before(checker.start) { + return false, fmt.Sprintf("obtained value %#v type must before start value of %#v", when, checker.start) + } + if when.After(checker.end) { + return false, fmt.Sprintf("obtained value %#v type must after end value of %#v", when, checker.end) + } + return true, "" +} diff -Nru juju-core-1.17.4/src/github.com/juju/ratelimit/LICENSE juju-core-1.17.6/src/github.com/juju/ratelimit/LICENSE --- juju-core-1.17.4/src/github.com/juju/ratelimit/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/ratelimit/LICENSE 2014-03-20 12:53:58.000000000 +0000 @@ -0,0 +1,185 @@ +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff -Nru juju-core-1.17.4/src/github.com/juju/ratelimit/ratelimit.go juju-core-1.17.6/src/github.com/juju/ratelimit/ratelimit.go --- juju-core-1.17.4/src/github.com/juju/ratelimit/ratelimit.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/ratelimit/ratelimit.go 2014-03-20 12:54:15.000000000 +0000 @@ -0,0 +1,97 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +// The ratelimit package provides an efficient token bucket implementation. +// See http://en.wikipedia.org/wiki/Token_bucket. +package ratelimit + +import ( + "sync" + "time" +) + +// TODO what about aborting requests? + +// TokenBucket represents a token bucket +// that fills at a predetermined rate. +// Methods on TokenBucket may be called +// concurrently. +type TokenBucket struct { + mu sync.Mutex + startTime time.Time + capacity int64 + fillInterval time.Duration + availTick int64 + avail int64 +} + +// New returns a new token bucket that fills at the +// rate of one token every fillInterval, up to the given +// maximum capacity. Both arguments must be +// positive. The bucket is initially full. +func New(fillInterval time.Duration, capacity int64) *TokenBucket { + if fillInterval <= 0 { + panic("token bucket fill interval is not > 0") + } + if capacity <= 0 { + panic("token bucket capacity is not > 0") + } + return &TokenBucket{ + startTime: time.Now(), + capacity: capacity, + avail: capacity, + fillInterval: fillInterval, + } +} + +// Wait takes count tokens from the bucket, +// waiting until they are available. +func (tb *TokenBucket) Wait(count int64) { + if d := tb.Take(count); d > 0 { + time.Sleep(d) + } +} + +// Take takes count tokens from the bucket without +// blocking. It returns the time that the caller should +// wait until the tokens are actually available. +// +// Note that if the request is irrevocable - there +// is no way to return tokens to the bucket once +// this method commits us to taking them. +func (tb *TokenBucket) Take(count int64) time.Duration { + return tb.take(time.Now(), count) +} + +// take is the internal version of Take - it takes +// the current time as an argument to enable easy testing. +func (tb *TokenBucket) take(now time.Time, count int64) time.Duration { + if count <= 0 { + return 0 + } + tb.mu.Lock() + defer tb.mu.Unlock() + currentTick := int64(now.Sub(tb.startTime) / tb.fillInterval) + tb.adjust(currentTick) + + tb.avail -= count + if tb.avail >= 0 { + return 0 + } + endTick := currentTick - tb.avail + endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval) + return endTime.Sub(now) +} + +// adjust adjusts the current bucket capacity based +// on the current tick. +func (tb *TokenBucket) adjust(currentTick int64) { + if tb.avail >= tb.capacity { + return + } + tb.avail += currentTick - tb.availTick + if tb.avail > tb.capacity { + tb.avail = tb.capacity + } + tb.availTick = currentTick +} diff -Nru juju-core-1.17.4/src/github.com/juju/ratelimit/ratelimit_test.go juju-core-1.17.6/src/github.com/juju/ratelimit/ratelimit_test.go --- juju-core-1.17.4/src/github.com/juju/ratelimit/ratelimit_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/ratelimit/ratelimit_test.go 2014-03-20 12:54:15.000000000 +0000 @@ -0,0 +1,144 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package ratelimit + +import ( + gc "launchpad.net/gocheck" + + "testing" + "time" +) + +func TestPackage(t *testing.T) { + gc.TestingT(t) +} + +type rateLimitSuite struct{} + +var _ = gc.Suite(rateLimitSuite{}) + +type req struct { + time time.Duration + count int64 + expectWait time.Duration +} + +var rateLimitTests = []struct { + about string + fillInterval time.Duration + capacity int64 + reqs []req +}{{ + about: "serial requests", + fillInterval: 250 * time.Millisecond, + capacity: 10, + reqs: []req{{ + time: 0, + count: 0, + expectWait: 0, + }, { + time: 0, + count: 10, + expectWait: 0, + }, { + time: 0, + count: 1, + expectWait: 250 * time.Millisecond, + }, { + time: 250 * time.Millisecond, + count: 1, + expectWait: 250 * time.Millisecond, + }}, +}, { + about: "concurrent requests", + fillInterval: 250 * time.Millisecond, + capacity: 10, + reqs: []req{{ + time: 0, + count: 10, + expectWait: 0, + }, { + time: 0, + count: 2, + expectWait: 500 * time.Millisecond, + }, { + time: 0, + count: 2, + expectWait: 1000 * time.Millisecond, + }, { + time: 0, + count: 1, + expectWait: 1250 * time.Millisecond, + }}, +}, { + about: "more than capacity", + fillInterval: 1 * time.Millisecond, + capacity: 10, + reqs: []req{{ + time: 0, + count: 10, + expectWait: 0, + }, { + time: 20 * time.Millisecond, + count: 15, + expectWait: 5 * time.Millisecond, + }}, +}, { + about: "sub-quantum time", + fillInterval: 10 * time.Millisecond, + capacity: 10, + reqs: []req{{ + time: 0, + count: 10, + expectWait: 0, + }, { + time: 7 * time.Millisecond, + count: 1, + expectWait: 3 * time.Millisecond, + }, { + time: 8 * time.Millisecond, + count: 1, + expectWait: 12 * time.Millisecond, + }}, +}, { + about: "within capacity", + fillInterval: 10 * time.Millisecond, + capacity: 5, + reqs: []req{{ + time: 0, + count: 5, + expectWait: 0, + }, { + time: 60 * time.Millisecond, + count: 5, + expectWait: 0, + }, { + time: 60 * time.Millisecond, + count: 1, + expectWait: 10 * time.Millisecond, + }, { + time: 80 * time.Millisecond, + count: 2, + expectWait: 10 * time.Millisecond, + }}, +}} + +func (rateLimitSuite) TestRateLimit(c *gc.C) { + for i, test := range rateLimitTests { + tb := New(test.fillInterval, test.capacity) + for j, req := range test.reqs { + d := tb.take(tb.startTime.Add(req.time), req.count) + if d != req.expectWait { + c.Fatalf("test %d.%d, %s, got %v want %v", i, j, test.about, d, req.expectWait) + } + } + } +} + +func (rateLimitSuite) TestPanics(c *gc.C) { + c.Assert(func() { New(0, 1) }, gc.PanicMatches, "token bucket fill interval is not > 0") + c.Assert(func() { New(-2, 1) }, gc.PanicMatches, "token bucket fill interval is not > 0") + c.Assert(func() { New(1, 0) }, gc.PanicMatches, "token bucket capacity is not > 0") + c.Assert(func() { New(1, -2) }, gc.PanicMatches, "token bucket capacity is not > 0") +} diff -Nru juju-core-1.17.4/src/github.com/juju/ratelimit/README.md juju-core-1.17.6/src/github.com/juju/ratelimit/README.md --- juju-core-1.17.4/src/github.com/juju/ratelimit/README.md 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/ratelimit/README.md 2014-03-20 12:54:15.000000000 +0000 @@ -0,0 +1,44 @@ +# ratelimit +-- + import "github.com/juju/ratelimit" + +The ratelimit package provides an efficient token bucket implementation. See +http://en.wikipedia.org/wiki/Token_bucket. + +## Usage + +#### type TokenBucket + +```go +type TokenBucket struct { +} +``` + +TokenBucket represents a token bucket that fills at a predetermined rate. +Methods on TokenBucket may be called concurrently. + +#### func New + +```go +func New(fillInterval time.Duration, capacity int64) *TokenBucket +``` +New returns a new token bucket that fills at the rate of one token every +fillInterval, up to the given maximum capacity. Both arguments must be positive. + +#### func (*TokenBucket) Take + +```go +func (tb *TokenBucket) Take(count int64) time.Duration +``` +Take takes count tokens from the bucket without blocking. It returns the time +that the caller should wait until the tokens are actually available. + +Note that if the request is irrevocable - there is no way to return tokens to +the bucket once this method commits us to taking them. + +#### func (*TokenBucket) Wait + +```go +func (tb *TokenBucket) Wait(count int64) +``` +Wait takes count tokens from the bucket, waiting until they are available. diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/bool.go juju-core-1.17.6/src/github.com/juju/testing/checkers/bool.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/bool.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/bool.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,115 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers + +import ( + "fmt" + "reflect" + + gc "launchpad.net/gocheck" +) + +type isTrueChecker struct { + *gc.CheckerInfo +} + +// IsTrue checks whether a value has an underlying +// boolean type and is true. +var IsTrue gc.Checker = &isTrueChecker{ + &gc.CheckerInfo{Name: "IsTrue", Params: []string{"obtained"}}, +} + +// IsTrue checks whether a value has an underlying +// boolean type and is false. +var IsFalse gc.Checker = gc.Not(IsTrue) + +func (checker *isTrueChecker) Check(params []interface{}, names []string) (result bool, error string) { + + value := reflect.ValueOf(params[0]) + + switch value.Kind() { + case reflect.Bool: + return value.Bool(), "" + } + + return false, fmt.Sprintf("expected type bool, received type %s", value.Type()) +} + +type satisfiesChecker struct { + *gc.CheckerInfo +} + +// Satisfies checks whether a value causes the argument +// function to return true. The function must be of +// type func(T) bool where the value being checked +// is assignable to T. +var Satisfies gc.Checker = &satisfiesChecker{ + &gc.CheckerInfo{ + Name: "Satisfies", + Params: []string{"obtained", "func(T) bool"}, + }, +} + +func (checker *satisfiesChecker) Check(params []interface{}, names []string) (result bool, error string) { + f := reflect.ValueOf(params[1]) + ft := f.Type() + if ft.Kind() != reflect.Func || + ft.NumIn() != 1 || + ft.NumOut() != 1 || + ft.Out(0) != reflect.TypeOf(true) { + return false, fmt.Sprintf("expected func(T) bool, got %s", ft) + } + v := reflect.ValueOf(params[0]) + if !v.IsValid() { + if !canBeNil(ft.In(0)) { + return false, fmt.Sprintf("cannot assign nil to argument %T", ft.In(0)) + } + v = reflect.Zero(ft.In(0)) + } + if !v.Type().AssignableTo(ft.In(0)) { + return false, fmt.Sprintf("wrong argument type %s for %s", v.Type(), ft) + } + return f.Call([]reflect.Value{v})[0].Interface().(bool), "" +} + +func canBeNil(t reflect.Type) bool { + switch t.Kind() { + case reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Ptr, + reflect.Slice: + return true + } + return false +} + +type deepEqualsChecker struct { + *gc.CheckerInfo +} + +// The DeepEquals checker verifies that the obtained value is deep-equal to +// the expected value. The check will work correctly even when facing +// slices, interfaces, and values of different types (which always fail +// the test). +// +// For example: +// +// c.Assert(value, DeepEquals, 42) +// c.Assert(array, DeepEquals, []string{"hi", "there"}) +// +// This checker differs from gocheck.DeepEquals in that +// it will compare a nil slice equal to an empty slice, +// and a nil map equal to an empty map. +var DeepEquals gc.Checker = &deepEqualsChecker{ + &gc.CheckerInfo{Name: "DeepEquals", Params: []string{"obtained", "expected"}}, +} + +func (checker *deepEqualsChecker) Check(params []interface{}, names []string) (result bool, error string) { + if ok, err := DeepEqual(params[0], params[1]); !ok { + return false, err.Error() + } + return true, "" +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/bool_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/bool_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/bool_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/bool_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,121 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers_test + +import ( + "errors" + "os" + + gc "launchpad.net/gocheck" + + jc "github.com/juju/testing/checkers" +) + +type BoolSuite struct{} + +var _ = gc.Suite(&BoolSuite{}) + +func (s *BoolSuite) TestIsTrue(c *gc.C) { + c.Assert(true, jc.IsTrue) + c.Assert(false, gc.Not(jc.IsTrue)) + + result, msg := jc.IsTrue.Check([]interface{}{false}, nil) + c.Assert(result, gc.Equals, false) + c.Assert(msg, gc.Equals, "") + + result, msg = jc.IsTrue.Check([]interface{}{"foo"}, nil) + c.Assert(result, gc.Equals, false) + c.Check(msg, gc.Equals, `expected type bool, received type string`) + + result, msg = jc.IsTrue.Check([]interface{}{42}, nil) + c.Assert(result, gc.Equals, false) + c.Assert(msg, gc.Equals, `expected type bool, received type int`) +} + +func (s *BoolSuite) TestIsFalse(c *gc.C) { + c.Assert(false, jc.IsFalse) + c.Assert(true, gc.Not(jc.IsFalse)) +} + +func is42(i int) bool { + return i == 42 +} + +var satisfiesTests = []struct { + f interface{} + arg interface{} + result bool + msg string +}{{ + f: is42, + arg: 42, + result: true, +}, { + f: is42, + arg: 41, + result: false, +}, { + f: is42, + arg: "", + result: false, + msg: "wrong argument type string for func(int) bool", +}, { + f: os.IsNotExist, + arg: errors.New("foo"), + result: false, +}, { + f: os.IsNotExist, + arg: os.ErrNotExist, + result: true, +}, { + f: os.IsNotExist, + arg: nil, + result: false, +}, { + f: func(chan int) bool { return true }, + arg: nil, + result: true, +}, { + f: func(func()) bool { return true }, + arg: nil, + result: true, +}, { + f: func(interface{}) bool { return true }, + arg: nil, + result: true, +}, { + f: func(map[string]bool) bool { return true }, + arg: nil, + result: true, +}, { + f: func(*int) bool { return true }, + arg: nil, + result: true, +}, { + f: func([]string) bool { return true }, + arg: nil, + result: true, +}} + +func (s *BoolSuite) TestSatisfies(c *gc.C) { + for i, test := range satisfiesTests { + c.Logf("test %d. %T %T", i, test.f, test.arg) + result, msg := jc.Satisfies.Check([]interface{}{test.arg, test.f}, nil) + c.Check(result, gc.Equals, test.result) + c.Check(msg, gc.Equals, test.msg) + } +} + +func (s *BoolSuite) TestDeepEquals(c *gc.C) { + for i, test := range deepEqualTests { + c.Logf("test %d. %v == %v is %v", i, test.a, test.b, test.eq) + result, msg := jc.DeepEquals.Check([]interface{}{test.a, test.b}, nil) + c.Check(result, gc.Equals, test.eq) + if test.eq { + c.Check(msg, gc.Equals, "") + } else { + c.Check(msg, gc.Not(gc.Equals), "") + } + } +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/checker.go juju-core-1.17.6/src/github.com/juju/testing/checkers/checker.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/checker.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/checker.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,202 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers + +import ( + "fmt" + "reflect" + "strings" + "time" + + gc "launchpad.net/gocheck" +) + +func TimeBetween(start, end time.Time) gc.Checker { + if end.Before(start) { + return &timeBetweenChecker{end, start} + } + return &timeBetweenChecker{start, end} +} + +type timeBetweenChecker struct { + start, end time.Time +} + +func (checker *timeBetweenChecker) Info() *gc.CheckerInfo { + info := gc.CheckerInfo{ + Name: "TimeBetween", + Params: []string{"obtained"}, + } + return &info +} + +func (checker *timeBetweenChecker) Check(params []interface{}, names []string) (result bool, error string) { + when, ok := params[0].(time.Time) + if !ok { + return false, "obtained value type must be time.Time" + } + if when.Before(checker.start) { + return false, fmt.Sprintf("obtained value %#v type must before start value of %#v", when, checker.start) + } + if when.After(checker.end) { + return false, fmt.Sprintf("obtained value %#v type must after end value of %#v", when, checker.end) + } + return true, "" +} + +// DurationLessThan checker + +type durationLessThanChecker struct { + *gc.CheckerInfo +} + +var DurationLessThan gc.Checker = &durationLessThanChecker{ + &gc.CheckerInfo{Name: "DurationLessThan", Params: []string{"obtained", "expected"}}, +} + +func (checker *durationLessThanChecker) Check(params []interface{}, names []string) (result bool, error string) { + obtained, ok := params[0].(time.Duration) + if !ok { + return false, "obtained value type must be time.Duration" + } + expected, ok := params[1].(time.Duration) + if !ok { + return false, "expected value type must be time.Duration" + } + return obtained.Nanoseconds() < expected.Nanoseconds(), "" +} + +// HasPrefix checker for checking strings + +func stringOrStringer(value interface{}) (string, bool) { + result, isString := value.(string) + if !isString { + if stringer, isStringer := value.(fmt.Stringer); isStringer { + result, isString = stringer.String(), true + } + } + return result, isString +} + +type hasPrefixChecker struct { + *gc.CheckerInfo +} + +var HasPrefix gc.Checker = &hasPrefixChecker{ + &gc.CheckerInfo{Name: "HasPrefix", Params: []string{"obtained", "expected"}}, +} + +func (checker *hasPrefixChecker) Check(params []interface{}, names []string) (result bool, error string) { + expected, ok := params[1].(string) + if !ok { + return false, "expected must be a string" + } + + obtained, isString := stringOrStringer(params[0]) + if isString { + return strings.HasPrefix(obtained, expected), "" + } + + return false, "Obtained value is not a string and has no .String()" +} + +type hasSuffixChecker struct { + *gc.CheckerInfo +} + +var HasSuffix gc.Checker = &hasSuffixChecker{ + &gc.CheckerInfo{Name: "HasSuffix", Params: []string{"obtained", "expected"}}, +} + +func (checker *hasSuffixChecker) Check(params []interface{}, names []string) (result bool, error string) { + expected, ok := params[1].(string) + if !ok { + return false, "expected must be a string" + } + + obtained, isString := stringOrStringer(params[0]) + if isString { + return strings.HasSuffix(obtained, expected), "" + } + + return false, "Obtained value is not a string and has no .String()" +} + +type containsChecker struct { + *gc.CheckerInfo +} + +var Contains gc.Checker = &containsChecker{ + &gc.CheckerInfo{Name: "Contains", Params: []string{"obtained", "expected"}}, +} + +func (checker *containsChecker) Check(params []interface{}, names []string) (result bool, error string) { + expected, ok := params[1].(string) + if !ok { + return false, "expected must be a string" + } + + obtained, isString := stringOrStringer(params[0]) + if isString { + return strings.Contains(obtained, expected), "" + } + + return false, "Obtained value is not a string and has no .String()" +} + +type sameContents struct { + *gc.CheckerInfo +} + +// SameContents checks that the obtained slice contains all the values (and +// same number of values) of the expected slice and vice versa, without respect +// to order or duplicates. Uses DeepEquals on mapped contents to compare. +var SameContents gc.Checker = &sameContents{ + &gc.CheckerInfo{Name: "SameContents", Params: []string{"obtained", "expected"}}, +} + +func (checker *sameContents) Check(params []interface{}, names []string) (result bool, error string) { + if len(params) != 2 { + return false, "SameContents expects two slice arguments" + } + obtained := params[0] + expected := params[1] + + tob := reflect.TypeOf(obtained) + if tob.Kind() != reflect.Slice { + return false, fmt.Sprintf("SameContents expects the obtained value to be a slice, got %q", + tob.Kind()) + } + + texp := reflect.TypeOf(expected) + if texp.Kind() != reflect.Slice { + return false, fmt.Sprintf("SameContents expects the expected value to be a slice, got %q", + texp.Kind()) + } + + if texp != tob { + return false, fmt.Sprintf( + "SameContents expects two slices of the same type, expected: %q, got: %q", + texp, tob) + } + + vexp := reflect.ValueOf(expected) + vob := reflect.ValueOf(obtained) + length := vexp.Len() + + if vob.Len() != length { + // Slice has incorrect number of elements + return false, "" + } + + // spin up maps with the entries as keys and the counts as values + mob := make(map[interface{}]int, length) + mexp := make(map[interface{}]int, length) + + for i := 0; i < length; i++ { + mexp[vexp.Index(i).Interface()]++ + mob[vob.Index(i).Interface()]++ + } + return reflect.DeepEqual(mob, mexp), "" +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/checker_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/checker_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/checker_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/checker_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,120 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers_test + +import ( + "testing" + + gc "launchpad.net/gocheck" + + jc "github.com/juju/testing/checkers" +) + +func Test(t *testing.T) { gc.TestingT(t) } + +type CheckerSuite struct{} + +var _ = gc.Suite(&CheckerSuite{}) + +func (s *CheckerSuite) TestHasPrefix(c *gc.C) { + c.Assert("foo bar", jc.HasPrefix, "foo") + c.Assert("foo bar", gc.Not(jc.HasPrefix), "omg") +} + +func (s *CheckerSuite) TestHasSuffix(c *gc.C) { + c.Assert("foo bar", jc.HasSuffix, "bar") + c.Assert("foo bar", gc.Not(jc.HasSuffix), "omg") +} + +func (s *CheckerSuite) TestContains(c *gc.C) { + c.Assert("foo bar baz", jc.Contains, "foo") + c.Assert("foo bar baz", jc.Contains, "bar") + c.Assert("foo bar baz", jc.Contains, "baz") + c.Assert("foo bar baz", gc.Not(jc.Contains), "omg") +} + +func (s *CheckerSuite) TestSameContents(c *gc.C) { + //// positive cases //// + + // same + c.Check( + []int{1, 2, 3}, jc.SameContents, + []int{1, 2, 3}) + + // empty + c.Check( + []int{}, jc.SameContents, + []int{}) + + // single + c.Check( + []int{1}, jc.SameContents, + []int{1}) + + // different order + c.Check( + []int{1, 2, 3}, jc.SameContents, + []int{3, 2, 1}) + + // multiple copies of same + c.Check( + []int{1, 1, 2}, jc.SameContents, + []int{2, 1, 1}) + + type test struct { + s string + i int + } + + // test structs + c.Check( + []test{{"a", 1}, {"b", 2}}, jc.SameContents, + []test{{"b", 2}, {"a", 1}}) + + //// negative cases //// + + // different contents + c.Check( + []int{1, 3, 2, 5}, gc.Not(jc.SameContents), + []int{5, 2, 3, 4}) + + // different size slices + c.Check( + []int{1, 2, 3}, gc.Not(jc.SameContents), + []int{1, 2}) + + // different counts of same items + c.Check( + []int{1, 1, 2}, gc.Not(jc.SameContents), + []int{1, 2, 2}) + + /// Error cases /// + // note: for these tests, we can't use gc.Not, since Not passes the error value through + // and checks with a non-empty error always count as failed + // Oddly, there doesn't seem to actually be a way to check for an error from a Checker. + + // different type + res, err := jc.SameContents.Check([]interface{}{ + []string{"1", "2"}, + []int{1, 2}, + }, []string{}) + c.Check(res, jc.IsFalse) + c.Check(err, gc.Not(gc.Equals), "") + + // obtained not a slice + res, err = jc.SameContents.Check([]interface{}{ + "test", + []int{1}, + }, []string{}) + c.Check(res, jc.IsFalse) + c.Check(err, gc.Not(gc.Equals), "") + + // expected not a slice + res, err = jc.SameContents.Check([]interface{}{ + []int{1}, + "test", + }, []string{}) + c.Check(res, jc.IsFalse) + c.Check(err, gc.Not(gc.Equals), "") +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/deepequal.go juju-core-1.17.6/src/github.com/juju/testing/checkers/deepequal.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/deepequal.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/deepequal.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,303 @@ +// Copied with small adaptations from the reflect package in the +// Go source tree. + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checkers + +import ( + "fmt" + "reflect" + "unsafe" +) + +// During deepValueEqual, must keep track of checks that are +// in progress. The comparison algorithm assumes that all +// checks in progress are true when it reencounters them. +// Visited comparisons are stored in a map indexed by visit. +type visit struct { + a1 uintptr + a2 uintptr + typ reflect.Type +} + +type mismatchError struct { + v1, v2 reflect.Value + path string + how string +} + +func (err *mismatchError) Error() string { + path := err.path + if path == "" { + path = "top level" + } + return fmt.Sprintf("mismatch at %s: %s; obtained %#v; expected %#v", path, err.how, interfaceOf(err.v1), interfaceOf(err.v2)) +} + +// Tests for deep equality using reflected types. The map argument tracks +// comparisons that have already been seen, which allows short circuiting on +// recursive types. +func deepValueEqual(path string, v1, v2 reflect.Value, visited map[visit]bool, depth int) (ok bool, err error) { + errorf := func(f string, a ...interface{}) error { + return &mismatchError{ + v1: v1, + v2: v2, + path: path, + how: fmt.Sprintf(f, a...), + } + } + if !v1.IsValid() || !v2.IsValid() { + if v1.IsValid() == v2.IsValid() { + return true, nil + } + return false, errorf("validity mismatch") + } + if v1.Type() != v2.Type() { + return false, errorf("type mismatch %s vs %s", v1.Type(), v2.Type()) + } + + // if depth > 10 { panic("deepValueEqual") } // for debugging + hard := func(k reflect.Kind) bool { + switch k { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: + return true + } + return false + } + + if v1.CanAddr() && v2.CanAddr() && hard(v1.Kind()) { + addr1 := v1.UnsafeAddr() + addr2 := v2.UnsafeAddr() + if addr1 > addr2 { + // Canonicalize order to reduce number of entries in visited. + addr1, addr2 = addr2, addr1 + } + + // Short circuit if references are identical ... + if addr1 == addr2 { + return true, nil + } + + // ... or already seen + typ := v1.Type() + v := visit{addr1, addr2, typ} + if visited[v] { + return true, nil + } + + // Remember for later. + visited[v] = true + } + + switch v1.Kind() { + case reflect.Array: + if v1.Len() != v2.Len() { + // can't happen! + return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) + } + for i := 0; i < v1.Len(); i++ { + if ok, err := deepValueEqual( + fmt.Sprintf("%s[%d]", path, i), + v1.Index(i), v2.Index(i), visited, depth+1); !ok { + return false, err + } + } + return true, nil + case reflect.Slice: + // We treat a nil slice the same as an empty slice. + if v1.Len() != v2.Len() { + return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) + } + if v1.Pointer() == v2.Pointer() { + return true, nil + } + for i := 0; i < v1.Len(); i++ { + if ok, err := deepValueEqual( + fmt.Sprintf("%s[%d]", path, i), + v1.Index(i), v2.Index(i), visited, depth+1); !ok { + return false, err + } + } + return true, nil + case reflect.Interface: + if v1.IsNil() || v2.IsNil() { + if v1.IsNil() != v2.IsNil() { + return false, fmt.Errorf("nil vs non-nil interface mismatch") + } + return true, nil + } + return deepValueEqual(path, v1.Elem(), v2.Elem(), visited, depth+1) + case reflect.Ptr: + return deepValueEqual("(*"+path+")", v1.Elem(), v2.Elem(), visited, depth+1) + case reflect.Struct: + for i, n := 0, v1.NumField(); i < n; i++ { + path := path + "." + v1.Type().Field(i).Name + if ok, err := deepValueEqual(path, v1.Field(i), v2.Field(i), visited, depth+1); !ok { + return false, err + } + } + return true, nil + case reflect.Map: + if v1.IsNil() != v2.IsNil() { + return false, errorf("nil vs non-nil mismatch") + } + if v1.Len() != v2.Len() { + return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) + } + if v1.Pointer() == v2.Pointer() { + return true, nil + } + for _, k := range v1.MapKeys() { + var p string + if k.CanInterface() { + p = path + "[" + fmt.Sprintf("%#v", k.Interface()) + "]" + } else { + p = path + "[someKey]" + } + if ok, err := deepValueEqual(p, v1.MapIndex(k), v2.MapIndex(k), visited, depth+1); !ok { + return false, err + } + } + return true, nil + case reflect.Func: + if v1.IsNil() && v2.IsNil() { + return true, nil + } + // Can't do better than this: + return false, errorf("non-nil functions") + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if v1.Int() != v2.Int() { + return false, errorf("unequal") + } + return true, nil + case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if v1.Uint() != v2.Uint() { + return false, errorf("unequal") + } + return true, nil + case reflect.Float32, reflect.Float64: + if v1.Float() != v2.Float() { + return false, errorf("unequal") + } + return true, nil + case reflect.Complex64, reflect.Complex128: + if v1.Complex() != v2.Complex() { + return false, errorf("unequal") + } + return true, nil + case reflect.Bool: + if v1.Bool() != v2.Bool() { + return false, errorf("unequal") + } + return true, nil + case reflect.String: + if v1.String() != v2.String() { + return false, errorf("unequal") + } + return true, nil + case reflect.Chan, reflect.UnsafePointer: + if v1.Pointer() != v2.Pointer() { + return false, errorf("unequal") + } + return true, nil + default: + panic("unexpected type " + v1.Type().String()) + } +} + +// DeepEqual tests for deep equality. It uses normal == equality where +// possible but will scan elements of arrays, slices, maps, and fields +// of structs. In maps, keys are compared with == but elements use deep +// equality. DeepEqual correctly handles recursive types. Functions are +// equal only if they are both nil. +// +// DeepEqual differs from reflect.DeepEqual in that an empty slice is +// equal to a nil slice. If the two values compare unequal, the +// resulting error holds the first difference encountered. +func DeepEqual(a1, a2 interface{}) (bool, error) { + errorf := func(f string, a ...interface{}) error { + return &mismatchError{ + v1: reflect.ValueOf(a1), + v2: reflect.ValueOf(a2), + path: "", + how: fmt.Sprintf(f, a...), + } + } + if a1 == nil || a2 == nil { + if a1 == a2 { + return true, nil + } + return false, errorf("nil vs non-nil mismatch") + } + v1 := reflect.ValueOf(a1) + v2 := reflect.ValueOf(a2) + if v1.Type() != v2.Type() { + return false, errorf("type mismatch %s vs %s", v1.Type(), v2.Type()) + } + return deepValueEqual("", v1, v2, make(map[visit]bool), 0) +} + +// interfaceOf returns v.Interface() even if v.CanInterface() == false. +// This enables us to call fmt.Printf on a value even if it's derived +// from inside an unexported field. +func interfaceOf(v reflect.Value) interface{} { + if !v.IsValid() { + return nil + } + return bypassCanInterface(v).Interface() +} + +type flag uintptr + +// copied from reflect/value.go +const ( + flagRO flag = 1 << iota +) + +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + return field.Offset +}() + +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) +} + +// bypassCanInterface returns a version of v that +// bypasses the CanInterface check. +func bypassCanInterface(v reflect.Value) reflect.Value { + if !v.IsValid() || v.CanInterface() { + return v + } + *flagField(&v) &^= flagRO + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + var t struct { + a int + A int + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + flagA := *flagField(&vA) + flaga := *flagField(&va) + if flagA&flagRO != 0 || flaga&flagRO == 0 { + panic("reflect.Value read-only flag has changed value") + } +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/deepequal_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/deepequal_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/deepequal_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/deepequal_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,176 @@ +// Copied with small adaptations from the reflect package in the +// Go source tree. We use testing rather than gocheck to preserve +// as much source equivalence as possible. + +// TODO tests for error messages + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checkers_test + +import ( + "regexp" + "testing" + + "github.com/juju/testing/checkers" +) + +func deepEqual(a1, a2 interface{}) bool { + ok, _ := checkers.DeepEqual(a1, a2) + return ok +} + +type Basic struct { + x int + y float32 +} + +type NotBasic Basic + +type DeepEqualTest struct { + a, b interface{} + eq bool + msg string +} + +// Simple functions for DeepEqual tests. +var ( + fn1 func() // nil. + fn2 func() // nil. + fn3 = func() { fn1() } // Not nil. +) + +var deepEqualTests = []DeepEqualTest{ + // Equalities + {nil, nil, true, ""}, + {1, 1, true, ""}, + {int32(1), int32(1), true, ""}, + {0.5, 0.5, true, ""}, + {float32(0.5), float32(0.5), true, ""}, + {"hello", "hello", true, ""}, + {make([]int, 10), make([]int, 10), true, ""}, + {&[3]int{1, 2, 3}, &[3]int{1, 2, 3}, true, ""}, + {Basic{1, 0.5}, Basic{1, 0.5}, true, ""}, + {error(nil), error(nil), true, ""}, + {map[int]string{1: "one", 2: "two"}, map[int]string{2: "two", 1: "one"}, true, ""}, + {fn1, fn2, true, ""}, + + // Inequalities + {1, 2, false, `mismatch at top level: unequal; obtained 1; expected 2`}, + {int32(1), int32(2), false, `mismatch at top level: unequal; obtained 1; expected 2`}, + {0.5, 0.6, false, `mismatch at top level: unequal; obtained 0\.5; expected 0\.6`}, + {float32(0.5), float32(0.6), false, `mismatch at top level: unequal; obtained 0\.5; expected 0\.6`}, + {"hello", "hey", false, `mismatch at top level: unequal; obtained "hello"; expected "hey"`}, + {make([]int, 10), make([]int, 11), false, `mismatch at top level: length mismatch, 10 vs 11; obtained \[\]int\{0, 0, 0, 0, 0, 0, 0, 0, 0, 0\}; expected \[\]int\{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\}`}, + {&[3]int{1, 2, 3}, &[3]int{1, 2, 4}, false, `mismatch at \(\*\)\[2\]: unequal; obtained 3; expected 4`}, + {Basic{1, 0.5}, Basic{1, 0.6}, false, `mismatch at \.y: unequal; obtained 0\.5; expected 0\.6`}, + {Basic{1, 0}, Basic{2, 0}, false, `mismatch at \.x: unequal; obtained 1; expected 2`}, + {map[int]string{1: "one", 3: "two"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at \[3\]: validity mismatch; obtained "two"; expected `}, + {map[int]string{1: "one", 2: "txo"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at \[2\]: unequal; obtained "txo"; expected "two"`}, + {map[int]string{1: "one"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at top level: length mismatch, 1 vs 2; obtained map\[int\]string\{1:"one"\}; expected map\[int\]string\{.*\}`}, + {map[int]string{2: "two", 1: "one"}, map[int]string{1: "one"}, false, `mismatch at top level: length mismatch, 2 vs 1; obtained map\[int\]string\{.*\}; expected map\[int\]string\{1:"one"\}`}, + {nil, 1, false, `mismatch at top level: nil vs non-nil mismatch; obtained ; expected 1`}, + {1, nil, false, `mismatch at top level: nil vs non-nil mismatch; obtained 1; expected `}, + {fn1, fn3, false, `mismatch at top level: non-nil functions; obtained \(func\(\)\)\(nil\); expected \(func\(\)\)\(0x[0-9a-f]+\)`}, + {fn3, fn3, false, `mismatch at top level: non-nil functions; obtained \(func\(\)\)\(0x[0-9a-f]+\); expected \(func\(\)\)\(0x[0-9a-f]+\)`}, + + // Nil vs empty: they're the same (difference from normal DeepEqual) + {[]int{}, []int(nil), true, ""}, + {[]int{}, []int{}, true, ""}, + {[]int(nil), []int(nil), true, ""}, + + // Mismatched types + {1, 1.0, false, `mismatch at top level: type mismatch int vs float64; obtained 1; expected 1`}, + {int32(1), int64(1), false, `mismatch at top level: type mismatch int32 vs int64; obtained 1; expected 1`}, + {0.5, "hello", false, `mismatch at top level: type mismatch float64 vs string; obtained 0\.5; expected "hello"`}, + {[]int{1, 2, 3}, [3]int{1, 2, 3}, false, `mismatch at top level: type mismatch \[\]int vs \[3\]int; obtained \[\]int\{1, 2, 3\}; expected \[3\]int\{1, 2, 3\}`}, + {&[3]interface{}{1, 2, 4}, &[3]interface{}{1, 2, "s"}, false, `mismatch at \(\*\)\[2\]: type mismatch int vs string; obtained 4; expected "s"`}, + {Basic{1, 0.5}, NotBasic{1, 0.5}, false, `mismatch at top level: type mismatch checkers_test\.Basic vs checkers_test\.NotBasic; obtained checkers_test\.Basic\{x:1, y:0\.5\}; expected checkers_test\.NotBasic\{x:1, y:0\.5\}`}, + { + map[uint]string{1: "one", 2: "two"}, + map[int]string{2: "two", 1: "one"}, + false, + `mismatch at top level: type mismatch map\[uint\]string vs map\[int\]string; obtained map\[uint\]string\{.*\}; expected map\[int\]string\{.*\}`, + }, +} + +func TestDeepEqual(t *testing.T) { + for _, test := range deepEqualTests { + r, err := checkers.DeepEqual(test.a, test.b) + if r != test.eq { + t.Errorf("deepEqual(%v, %v) = %v, want %v", test.a, test.b, r, test.eq) + } + if test.eq { + if err != nil { + t.Errorf("deepEqual(%v, %v): unexpected error message %q when equal", test.a, test.b, err) + } + } else { + if ok, _ := regexp.MatchString(test.msg, err.Error()); !ok { + t.Errorf("deepEqual(%v, %v); unexpected error %q, want %q", test.a, test.b, err.Error(), test.msg) + } + } + } +} + +type Recursive struct { + x int + r *Recursive +} + +func TestDeepEqualRecursiveStruct(t *testing.T) { + a, b := new(Recursive), new(Recursive) + *a = Recursive{12, a} + *b = Recursive{12, b} + if !deepEqual(a, b) { + t.Error("deepEqual(recursive same) = false, want true") + } +} + +type _Complex struct { + a int + b [3]*_Complex + c *string + d map[float64]float64 +} + +func TestDeepEqualComplexStruct(t *testing.T) { + m := make(map[float64]float64) + stra, strb := "hello", "hello" + a, b := new(_Complex), new(_Complex) + *a = _Complex{5, [3]*_Complex{a, b, a}, &stra, m} + *b = _Complex{5, [3]*_Complex{b, a, a}, &strb, m} + if !deepEqual(a, b) { + t.Error("deepEqual(complex same) = false, want true") + } +} + +func TestDeepEqualComplexStructInequality(t *testing.T) { + m := make(map[float64]float64) + stra, strb := "hello", "helloo" // Difference is here + a, b := new(_Complex), new(_Complex) + *a = _Complex{5, [3]*_Complex{a, b, a}, &stra, m} + *b = _Complex{5, [3]*_Complex{b, a, a}, &strb, m} + if deepEqual(a, b) { + t.Error("deepEqual(complex different) = true, want false") + } +} + +type UnexpT struct { + m map[int]int +} + +func TestDeepEqualUnexportedMap(t *testing.T) { + // Check that DeepEqual can look at unexported fields. + x1 := UnexpT{map[int]int{1: 2}} + x2 := UnexpT{map[int]int{1: 2}} + if !deepEqual(&x1, &x2) { + t.Error("deepEqual(x1, x2) = false, want true") + } + + y1 := UnexpT{map[int]int{2: 3}} + if deepEqual(&x1, &y1) { + t.Error("deepEqual(x1, y1) = true, want false") + } +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/file.go juju-core-1.17.6/src/github.com/juju/testing/checkers/file.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/file.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/file.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,154 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers + +import ( + "fmt" + "os" + "reflect" + + gc "launchpad.net/gocheck" +) + +// IsNonEmptyFile checker + +type isNonEmptyFileChecker struct { + *gc.CheckerInfo +} + +var IsNonEmptyFile gc.Checker = &isNonEmptyFileChecker{ + &gc.CheckerInfo{Name: "IsNonEmptyFile", Params: []string{"obtained"}}, +} + +func (checker *isNonEmptyFileChecker) Check(params []interface{}, names []string) (result bool, error string) { + filename, isString := stringOrStringer(params[0]) + if isString { + fileInfo, err := os.Stat(filename) + if os.IsNotExist(err) { + return false, fmt.Sprintf("%s does not exist", filename) + } else if err != nil { + return false, fmt.Sprintf("other stat error: %v", err) + } + if fileInfo.Size() > 0 { + return true, "" + } else { + return false, fmt.Sprintf("%s is empty", filename) + } + } + + value := reflect.ValueOf(params[0]) + return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) +} + +// IsDirectory checker + +type isDirectoryChecker struct { + *gc.CheckerInfo +} + +var IsDirectory gc.Checker = &isDirectoryChecker{ + &gc.CheckerInfo{Name: "IsDirectory", Params: []string{"obtained"}}, +} + +func (checker *isDirectoryChecker) Check(params []interface{}, names []string) (result bool, error string) { + path, isString := stringOrStringer(params[0]) + if isString { + fileInfo, err := os.Stat(path) + if os.IsNotExist(err) { + return false, fmt.Sprintf("%s does not exist", path) + } else if err != nil { + return false, fmt.Sprintf("other stat error: %v", err) + } + if fileInfo.IsDir() { + return true, "" + } else { + return false, fmt.Sprintf("%s is not a directory", path) + } + } + + value := reflect.ValueOf(params[0]) + return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) +} + +// IsSymlink checker + +type isSymlinkChecker struct { + *gc.CheckerInfo +} + +var IsSymlink gc.Checker = &isSymlinkChecker{ + &gc.CheckerInfo{Name: "IsSymlink", Params: []string{"obtained"}}, +} + +func (checker *isSymlinkChecker) Check(params []interface{}, names []string) (result bool, error string) { + path, isString := stringOrStringer(params[0]) + if isString { + fileInfo, err := os.Lstat(path) + if os.IsNotExist(err) { + return false, fmt.Sprintf("%s does not exist", path) + } else if err != nil { + return false, fmt.Sprintf("other stat error: %v", err) + } + if fileInfo.Mode()&os.ModeSymlink != 0 { + return true, "" + } else { + return false, fmt.Sprintf("%s is not a symlink: %+v", path, fileInfo) + } + } + + value := reflect.ValueOf(params[0]) + return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) +} + +// DoesNotExist checker makes sure the path specified doesn't exist. + +type doesNotExistChecker struct { + *gc.CheckerInfo +} + +var DoesNotExist gc.Checker = &doesNotExistChecker{ + &gc.CheckerInfo{Name: "DoesNotExist", Params: []string{"obtained"}}, +} + +func (checker *doesNotExistChecker) Check(params []interface{}, names []string) (result bool, error string) { + path, isString := stringOrStringer(params[0]) + if isString { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return true, "" + } else if err != nil { + return false, fmt.Sprintf("other stat error: %v", err) + } + return false, fmt.Sprintf("%s exists", path) + } + + value := reflect.ValueOf(params[0]) + return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) +} + +// SymlinkDoesNotExist checker makes sure the path specified doesn't exist. + +type symlinkDoesNotExistChecker struct { + *gc.CheckerInfo +} + +var SymlinkDoesNotExist gc.Checker = &symlinkDoesNotExistChecker{ + &gc.CheckerInfo{Name: "SymlinkDoesNotExist", Params: []string{"obtained"}}, +} + +func (checker *symlinkDoesNotExistChecker) Check(params []interface{}, names []string) (result bool, error string) { + path, isString := stringOrStringer(params[0]) + if isString { + _, err := os.Lstat(path) + if os.IsNotExist(err) { + return true, "" + } else if err != nil { + return false, fmt.Sprintf("other stat error: %v", err) + } + return false, fmt.Sprintf("%s exists", path) + } + + value := reflect.ValueOf(params[0]) + return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/file_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/file_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/file_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/file_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,166 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + gc "launchpad.net/gocheck" + + jc "github.com/juju/testing/checkers" +) + +type FileSuite struct{} + +var _ = gc.Suite(&FileSuite{}) + +func (s *FileSuite) TestIsNonEmptyFile(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, gc.IsNil) + fmt.Fprintf(file, "something") + file.Close() + + c.Assert(file.Name(), jc.IsNonEmptyFile) +} + +func (s *FileSuite) TestIsNonEmptyFileWithEmptyFile(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, gc.IsNil) + file.Close() + + result, message := jc.IsNonEmptyFile.Check([]interface{}{file.Name()}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, file.Name()+" is empty") +} + +func (s *FileSuite) TestIsNonEmptyFileWithMissingFile(c *gc.C) { + name := filepath.Join(c.MkDir(), "missing") + + result, message := jc.IsNonEmptyFile.Check([]interface{}{name}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, name+" does not exist") +} + +func (s *FileSuite) TestIsNonEmptyFileWithNumber(c *gc.C) { + result, message := jc.IsNonEmptyFile.Check([]interface{}{42}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") +} + +func (s *FileSuite) TestIsDirectory(c *gc.C) { + dir := c.MkDir() + c.Assert(dir, jc.IsDirectory) +} + +func (s *FileSuite) TestIsDirectoryMissing(c *gc.C) { + absentDir := filepath.Join(c.MkDir(), "foo") + + result, message := jc.IsDirectory.Check([]interface{}{absentDir}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, absentDir+" does not exist") +} + +func (s *FileSuite) TestIsDirectoryWithFile(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, gc.IsNil) + file.Close() + + result, message := jc.IsDirectory.Check([]interface{}{file.Name()}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, file.Name()+" is not a directory") +} + +func (s *FileSuite) TestIsDirectoryWithNumber(c *gc.C) { + result, message := jc.IsDirectory.Check([]interface{}{42}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") +} + +func (s *FileSuite) TestDoesNotExist(c *gc.C) { + absentDir := filepath.Join(c.MkDir(), "foo") + c.Assert(absentDir, jc.DoesNotExist) +} + +func (s *FileSuite) TestDoesNotExistWithPath(c *gc.C) { + dir := c.MkDir() + result, message := jc.DoesNotExist.Check([]interface{}{dir}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, dir+" exists") +} + +func (s *FileSuite) TestDoesNotExistWithSymlink(c *gc.C) { + dir := c.MkDir() + deadPath := filepath.Join(dir, "dead") + symlinkPath := filepath.Join(dir, "a-symlink") + err := os.Symlink(deadPath, symlinkPath) + c.Assert(err, gc.IsNil) + // A valid symlink pointing to something that doesn't exist passes. + // Use SymlinkDoesNotExist to check for the non-existence of the link itself. + c.Assert(symlinkPath, jc.DoesNotExist) +} + +func (s *FileSuite) TestDoesNotExistWithNumber(c *gc.C) { + result, message := jc.DoesNotExist.Check([]interface{}{42}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") +} + +func (s *FileSuite) TestSymlinkDoesNotExist(c *gc.C) { + absentDir := filepath.Join(c.MkDir(), "foo") + c.Assert(absentDir, jc.SymlinkDoesNotExist) +} + +func (s *FileSuite) TestSymlinkDoesNotExistWithPath(c *gc.C) { + dir := c.MkDir() + result, message := jc.SymlinkDoesNotExist.Check([]interface{}{dir}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, dir+" exists") +} + +func (s *FileSuite) TestSymlinkDoesNotExistWithSymlink(c *gc.C) { + dir := c.MkDir() + deadPath := filepath.Join(dir, "dead") + symlinkPath := filepath.Join(dir, "a-symlink") + err := os.Symlink(deadPath, symlinkPath) + c.Assert(err, gc.IsNil) + + result, message := jc.SymlinkDoesNotExist.Check([]interface{}{symlinkPath}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, symlinkPath+" exists") +} + +func (s *FileSuite) TestSymlinkDoesNotExistWithNumber(c *gc.C) { + result, message := jc.SymlinkDoesNotExist.Check([]interface{}{42}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") +} + +func (s *FileSuite) TestIsSymlink(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, gc.IsNil) + c.Log(file.Name()) + c.Log(filepath.Dir(file.Name())) + symlinkPath := filepath.Join(filepath.Dir(file.Name()), "a-symlink") + err = os.Symlink(file.Name(), symlinkPath) + c.Assert(err, gc.IsNil) + + c.Assert(symlinkPath, jc.IsSymlink) +} + +func (s *FileSuite) TestIsSymlinkWithFile(c *gc.C) { + file, err := ioutil.TempFile(c.MkDir(), "") + c.Assert(err, gc.IsNil) + result, message := jc.IsSymlink.Check([]interface{}{file.Name()}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, jc.Contains, " is not a symlink") +} + +func (s *FileSuite) TestIsSymlinkWithDir(c *gc.C) { + result, message := jc.IsSymlink.Check([]interface{}{c.MkDir()}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(message, jc.Contains, " is not a symlink") +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/log.go juju-core-1.17.6/src/github.com/juju/testing/checkers/log.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/log.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/log.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,109 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers + +import ( + "fmt" + "regexp" + "strings" + + "github.com/juju/loggo" + gc "launchpad.net/gocheck" +) + +type SimpleMessage struct { + Level loggo.Level + Message string +} + +type SimpleMessages []SimpleMessage + +func (s SimpleMessage) String() string { + return fmt.Sprintf("%s %s", s.Level, s.Message) +} + +func (s SimpleMessages) GoString() string { + out := make([]string, len(s)) + for i, m := range s { + out[i] = m.String() + } + return fmt.Sprintf("SimpleMessages{\n%s\n}", strings.Join(out, "\n")) +} + +func logToSimpleMessages(log []loggo.TestLogValues) SimpleMessages { + out := make(SimpleMessages, len(log)) + for i, val := range log { + out[i].Level = val.Level + out[i].Message = val.Message + } + return out +} + +type logMatches struct { + *gc.CheckerInfo +} + +func (checker *logMatches) Check(params []interface{}, names []string) (result bool, error string) { + var obtained SimpleMessages + switch params[0].(type) { + case []loggo.TestLogValues: + obtained = logToSimpleMessages(params[0].([]loggo.TestLogValues)) + default: + return false, "Obtained value must be of type []loggo.TestLogValues or SimpleMessage" + } + + var expected SimpleMessages + switch param := params[1].(type) { + case []SimpleMessage: + expected = SimpleMessages(param) + case SimpleMessages: + expected = param + case []string: + expected = make(SimpleMessages, len(param)) + for i, s := range param { + expected[i] = SimpleMessage{ + Message: s, + Level: loggo.UNSPECIFIED, + } + } + default: + return false, "Expected value must be of type []string or []SimpleMessage" + } + + obtainedSinceLastMatch := obtained + for len(expected) > 0 && len(obtained) >= len(expected) { + var msg SimpleMessage + msg, obtained = obtained[0], obtained[1:] + expect := expected[0] + if expect.Level != loggo.UNSPECIFIED && msg.Level != expect.Level { + continue + } + matched, err := regexp.MatchString(expect.Message, msg.Message) + if err != nil { + return false, fmt.Sprintf("bad message regexp %q: %v", expect.Message, err) + } else if !matched { + continue + } + expected = expected[1:] + obtainedSinceLastMatch = obtained + } + if len(obtained) < len(expected) { + params[0] = obtainedSinceLastMatch + params[1] = expected + return false, "" + } + return true, "" +} + +// LogMatches checks whether a given TestLogValues actually contains the log +// messages we expected. If you compare it against a list of strings, we only +// compare that the strings in the messages are correct. You can alternatively +// pass a slice of SimpleMessage and we will check that the log levels are +// also correct. +// +// The log may contain additional messages before and after each of the specified +// expected messages. +var LogMatches gc.Checker = &logMatches{ + &gc.CheckerInfo{Name: "LogMatches", Params: []string{"obtained", "expected"}}, +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/log_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/log_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/log_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/log_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,112 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers_test + +import ( + "github.com/juju/loggo" + gc "launchpad.net/gocheck" + + jc "github.com/juju/testing/checkers" +) + +type LogMatchesSuite struct{} + +var _ = gc.Suite(&LogMatchesSuite{}) + +func (s *LogMatchesSuite) TestMatchSimpleMessage(c *gc.C) { + log := []loggo.TestLogValues{ + {Level: loggo.INFO, Message: "foo bar"}, + {Level: loggo.INFO, Message: "12345"}, + } + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.INFO, "foo bar"}, + {loggo.INFO, "12345"}, + }) + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.INFO, "foo .*"}, + {loggo.INFO, "12345"}, + }) + // UNSPECIFIED means we don't care what the level is, + // just check the message string matches. + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.UNSPECIFIED, "foo .*"}, + {loggo.INFO, "12345"}, + }) + c.Check(log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ + {loggo.INFO, "foo bar"}, + {loggo.DEBUG, "12345"}, + }) +} + +func (s *LogMatchesSuite) TestMatchStrings(c *gc.C) { + log := []loggo.TestLogValues{ + {Level: loggo.INFO, Message: "foo bar"}, + {Level: loggo.INFO, Message: "12345"}, + } + c.Check(log, jc.LogMatches, []string{"foo bar", "12345"}) + c.Check(log, jc.LogMatches, []string{"foo .*", "12345"}) + c.Check(log, gc.Not(jc.LogMatches), []string{"baz", "bing"}) +} + +func (s *LogMatchesSuite) TestMatchInexact(c *gc.C) { + log := []loggo.TestLogValues{ + {Level: loggo.INFO, Message: "foo bar"}, + {Level: loggo.INFO, Message: "baz"}, + {Level: loggo.DEBUG, Message: "12345"}, + {Level: loggo.ERROR, Message: "12345"}, + {Level: loggo.INFO, Message: "67890"}, + } + c.Check(log, jc.LogMatches, []string{"foo bar", "12345"}) + c.Check(log, jc.LogMatches, []string{"foo .*", "12345"}) + c.Check(log, jc.LogMatches, []string{"foo .*", "67890"}) + c.Check(log, jc.LogMatches, []string{"67890"}) + + // Matches are always left-most after the previous match. + c.Check(log, jc.LogMatches, []string{".*", "baz"}) + c.Check(log, jc.LogMatches, []string{"foo bar", ".*", "12345"}) + c.Check(log, jc.LogMatches, []string{"foo bar", ".*", "67890"}) + + // Order is important: 67890 advances to the last item in obtained, + // and so there's nothing after to match against ".*". + c.Check(log, gc.Not(jc.LogMatches), []string{"67890", ".*"}) + // ALL specified patterns MUST match in the order given. + c.Check(log, gc.Not(jc.LogMatches), []string{".*", "foo bar"}) + + // Check that levels are matched. + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.UNSPECIFIED, "12345"}, + {loggo.UNSPECIFIED, "12345"}, + }) + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.DEBUG, "12345"}, + {loggo.ERROR, "12345"}, + }) + c.Check(log, jc.LogMatches, []jc.SimpleMessage{ + {loggo.DEBUG, "12345"}, + {loggo.INFO, ".*"}, + }) + c.Check(log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ + {loggo.DEBUG, "12345"}, + {loggo.INFO, ".*"}, + {loggo.UNSPECIFIED, ".*"}, + }) +} + +func (s *LogMatchesSuite) TestFromLogMatches(c *gc.C) { + tw := &loggo.TestWriter{} + _, err := loggo.ReplaceDefaultWriter(tw) + c.Assert(err, gc.IsNil) + defer loggo.ResetWriters() + logger := loggo.GetLogger("test") + logger.SetLogLevel(loggo.DEBUG) + logger.Infof("foo") + logger.Debugf("bar") + logger.Tracef("hidden") + c.Check(tw.Log, jc.LogMatches, []string{"foo", "bar"}) + c.Check(tw.Log, gc.Not(jc.LogMatches), []string{"foo", "bad"}) + c.Check(tw.Log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ + {loggo.INFO, "foo"}, + {loggo.INFO, "bar"}, + }) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/relop.go juju-core-1.17.6/src/github.com/juju/testing/checkers/relop.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/relop.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/relop.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,93 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers + +import ( + "fmt" + "reflect" + + gc "launchpad.net/gocheck" +) + +// GreaterThan checker + +type greaterThanChecker struct { + *gc.CheckerInfo +} + +var GreaterThan gc.Checker = &greaterThanChecker{ + &gc.CheckerInfo{Name: "GreaterThan", Params: []string{"obtained", "expected"}}, +} + +func (checker *greaterThanChecker) Check(params []interface{}, names []string) (result bool, error string) { + defer func() { + if v := recover(); v != nil { + result = false + error = fmt.Sprint(v) + } + }() + + p0value := reflect.ValueOf(params[0]) + p1value := reflect.ValueOf(params[1]) + switch p0value.Kind() { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64: + return p0value.Int() > p1value.Int(), "" + case reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + return p0value.Uint() > p1value.Uint(), "" + case reflect.Float32, + reflect.Float64: + return p0value.Float() > p1value.Float(), "" + default: + } + return false, fmt.Sprintf("obtained value %s:%#v not supported", p0value.Kind(), params[0]) +} + +// LessThan checker + +type lessThanChecker struct { + *gc.CheckerInfo +} + +var LessThan gc.Checker = &lessThanChecker{ + &gc.CheckerInfo{Name: "LessThan", Params: []string{"obtained", "expected"}}, +} + +func (checker *lessThanChecker) Check(params []interface{}, names []string) (result bool, error string) { + defer func() { + if v := recover(); v != nil { + result = false + error = fmt.Sprint(v) + } + }() + + p0value := reflect.ValueOf(params[0]) + p1value := reflect.ValueOf(params[1]) + switch p0value.Kind() { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64: + return p0value.Int() < p1value.Int(), "" + case reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + return p0value.Uint() < p1value.Uint(), "" + case reflect.Float32, + reflect.Float64: + return p0value.Float() < p1value.Float(), "" + default: + } + return false, fmt.Sprintf("obtained value %s:%#v not supported", p0value.Kind(), params[0]) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/checkers/relop_test.go juju-core-1.17.6/src/github.com/juju/testing/checkers/relop_test.go --- juju-core-1.17.4/src/github.com/juju/testing/checkers/relop_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/checkers/relop_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,36 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package checkers_test + +import ( + gc "launchpad.net/gocheck" + + jc "github.com/juju/testing/checkers" +) + +type RelopSuite struct{} + +var _ = gc.Suite(&RelopSuite{}) + +func (s *RelopSuite) TestGreaterThan(c *gc.C) { + c.Assert(45, jc.GreaterThan, 42) + c.Assert(2.25, jc.GreaterThan, 1.0) + c.Assert(42, gc.Not(jc.GreaterThan), 42) + c.Assert(10, gc.Not(jc.GreaterThan), 42) + + result, msg := jc.GreaterThan.Check([]interface{}{"Hello", "World"}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(msg, gc.Equals, `obtained value string:"Hello" not supported`) +} + +func (s *RelopSuite) TestLessThan(c *gc.C) { + c.Assert(42, jc.LessThan, 45) + c.Assert(1.0, jc.LessThan, 2.25) + c.Assert(42, gc.Not(jc.LessThan), 42) + c.Assert(42, gc.Not(jc.LessThan), 10) + + result, msg := jc.LessThan.Check([]interface{}{"Hello", "World"}, nil) + c.Assert(result, jc.IsFalse) + c.Assert(msg, gc.Equals, `obtained value string:"Hello" not supported`) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/cleanup.go juju-core-1.17.6/src/github.com/juju/testing/cleanup.go --- juju-core-1.17.4/src/github.com/juju/testing/cleanup.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/cleanup.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,92 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing + +import ( + "os/exec" + + gc "launchpad.net/gocheck" +) + +type CleanupFunc func(*gc.C) +type cleanupStack []CleanupFunc + +// CleanupSuite adds the ability to add cleanup functions that are called +// during either test tear down or suite tear down depending on the method +// called. +type CleanupSuite struct { + testStack cleanupStack + suiteStack cleanupStack +} + +func (s *CleanupSuite) SetUpSuite(c *gc.C) { + s.suiteStack = nil +} + +func (s *CleanupSuite) TearDownSuite(c *gc.C) { + s.callStack(c, s.suiteStack) +} + +func (s *CleanupSuite) SetUpTest(c *gc.C) { + s.testStack = nil +} + +func (s *CleanupSuite) TearDownTest(c *gc.C) { + s.callStack(c, s.testStack) +} + +func (s *CleanupSuite) callStack(c *gc.C, stack cleanupStack) { + for i := len(stack) - 1; i >= 0; i-- { + stack[i](c) + } +} + +// AddCleanup pushes the cleanup function onto the stack of functions to be +// called during TearDownTest. +func (s *CleanupSuite) AddCleanup(cleanup CleanupFunc) { + s.testStack = append(s.testStack, cleanup) +} + +// AddSuiteCleanup pushes the cleanup function onto the stack of functions to +// be called during TearDownSuite. +func (s *CleanupSuite) AddSuiteCleanup(cleanup CleanupFunc) { + s.suiteStack = append(s.suiteStack, cleanup) +} + +// PatchEnvironment sets the environment variable 'name' the the value passed +// in. The old value is saved and returned to the original value at test tear +// down time using a cleanup function. +func (s *CleanupSuite) PatchEnvironment(name, value string) { + restore := PatchEnvironment(name, value) + s.AddCleanup(func(*gc.C) { restore() }) +} + +// PatchEnvPathPrepend prepends the given path to the environment $PATH and restores the +// original path on test teardown. +func (s *CleanupSuite) PatchEnvPathPrepend(dir string) { + restore := PatchEnvPathPrepend(dir) + s.AddCleanup(func(*gc.C) { restore() }) +} + +// PatchValue sets the 'dest' variable the the value passed in. The old value +// is saved and returned to the original value at test tear down time using a +// cleanup function. The value must be assignable to the element type of the +// destination. +func (s *CleanupSuite) PatchValue(dest, value interface{}) { + restore := PatchValue(dest, value) + s.AddCleanup(func(*gc.C) { restore() }) +} + +// HookCommandOutput calls the package function of the same name to mock out +// the result of a particular comand execution, and will call the restore +// function on test teardown. +func (s *CleanupSuite) HookCommandOutput( + outputFunc *func(cmd *exec.Cmd) ([]byte, error), + output []byte, + err error, +) <-chan *exec.Cmd { + result, restore := HookCommandOutput(outputFunc, output, err) + s.AddCleanup(func(*gc.C) { restore() }) + return result +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/cleanup_test.go juju-core-1.17.6/src/github.com/juju/testing/cleanup_test.go --- juju-core-1.17.4/src/github.com/juju/testing/cleanup_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/cleanup_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,115 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing_test + +import ( + "os" + + gc "launchpad.net/gocheck" + + "github.com/juju/testing" +) + +type cleanupSuite struct { + testing.CleanupSuite +} + +var _ = gc.Suite(&cleanupSuite{}) + +func (s *cleanupSuite) TestTearDownSuiteEmpty(c *gc.C) { + // The suite stack is empty initially, check we can tear that down. + s.TearDownSuite(c) + s.SetUpSuite(c) +} + +func (s *cleanupSuite) TestTearDownTestEmpty(c *gc.C) { + // The test stack is empty initially, check we can tear that down. + s.TearDownTest(c) + s.SetUpTest(c) +} + +func (s *cleanupSuite) TestAddSuiteCleanup(c *gc.C) { + order := []string{} + s.AddSuiteCleanup(func(*gc.C) { + order = append(order, "first") + }) + s.AddSuiteCleanup(func(*gc.C) { + order = append(order, "second") + }) + + s.TearDownSuite(c) + c.Assert(order, gc.DeepEquals, []string{"second", "first"}) + + // SetUpSuite resets the cleanup stack, this stops the cleanup functions + // being called again. + s.SetUpSuite(c) +} + +func (s *cleanupSuite) TestAddCleanup(c *gc.C) { + order := []string{} + s.AddCleanup(func(*gc.C) { + order = append(order, "first") + }) + s.AddCleanup(func(*gc.C) { + order = append(order, "second") + }) + + s.TearDownTest(c) + c.Assert(order, gc.DeepEquals, []string{"second", "first"}) + + // SetUpTest resets the cleanup stack, this stops the cleanup functions + // being called again. + s.SetUpTest(c) +} + +func (s *cleanupSuite) TestPatchEnvironment(c *gc.C) { + const envName = "TESTING_PATCH_ENVIRONMENT" + // remember the old value, and set it to something we can check + oldValue := os.Getenv(envName) + os.Setenv(envName, "initial") + + s.PatchEnvironment(envName, "new value") + // Using check to make sure the environment gets set back properly in the test. + c.Check(os.Getenv(envName), gc.Equals, "new value") + + s.TearDownTest(c) + c.Check(os.Getenv(envName), gc.Equals, "initial") + + // SetUpTest resets the cleanup stack, this stops the cleanup functions + // being called again. + s.SetUpTest(c) + // explicitly return the envName to the old value + os.Setenv(envName, oldValue) +} + +func (s *cleanupSuite) TestPatchValueInt(c *gc.C) { + i := 42 + s.PatchValue(&i, 0) + c.Assert(i, gc.Equals, 0) + + s.TearDownTest(c) + c.Assert(i, gc.Equals, 42) + + // SetUpTest resets the cleanup stack, this stops the cleanup functions + // being called again. + s.SetUpTest(c) +} + +func (s *cleanupSuite) TestPatchValueFunction(c *gc.C) { + function := func() string { + return "original" + } + + s.PatchValue(&function, func() string { + return "patched" + }) + c.Assert(function(), gc.Equals, "patched") + + s.TearDownTest(c) + c.Assert(function(), gc.Equals, "original") + + // SetUpTest resets the cleanup stack, this stops the cleanup functions + // being called again. + s.SetUpTest(c) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/cmd.go juju-core-1.17.6/src/github.com/juju/testing/cmd.go --- juju-core-1.17.4/src/github.com/juju/testing/cmd.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/cmd.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,94 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + gc "launchpad.net/gocheck" +) + +// HookCommandOutput intercepts CommandOutput to a function that passes the +// actual command and it's output back via a channel, and returns the error +// passed into this function. It also returns a cleanup function so you can +// restore the original function +func HookCommandOutput( + outputFunc *func(cmd *exec.Cmd) ([]byte, error), output []byte, err error) (<-chan *exec.Cmd, func()) { + + cmdChan := make(chan *exec.Cmd, 1) + origCommandOutput := *outputFunc + cleanup := func() { + close(cmdChan) + *outputFunc = origCommandOutput + } + *outputFunc = func(cmd *exec.Cmd) ([]byte, error) { + cmdChan <- cmd + return output, err + } + return cmdChan, cleanup +} + +const ( + // EchoQuotedArgs is a simple bash script that prints out the + // basename of the command followed by the args as quoted strings. + EchoQuotedArgs = `#!/bin/bash --norc +name=` + "`basename $0`" + ` +argfile="$name.out" +rm -f $argfile +printf "%s" $name | tee -a $argfile +for arg in "$@"; do + printf " \"%s\"" "$arg" | tee -a $argfile +done +printf "\n" | tee -a $argfile +` +) + +// EnvironmentPatcher is an interface that requires just one method: +// PatchEnvironment. +type EnvironmentPatcher interface { + PatchEnvironment(name, value string) +} + +// PatchExecutable creates an executable called 'execName' in a new test +// directory and that directory is added to the path. +func PatchExecutable(c *gc.C, patcher EnvironmentPatcher, execName, script string) { + dir := c.MkDir() + patcher.PatchEnvironment("PATH", joinPathLists(dir, os.Getenv("PATH"))) + filename := filepath.Join(dir, execName) + err := ioutil.WriteFile(filename, []byte(script), 0755) + c.Assert(err, gc.IsNil) +} + +type CleanupPatcher interface { + PatchEnvironment(name, value string) + AddCleanup(cleanup CleanupFunc) +} + +// PatchExecutableAsEchoArgs creates an executable called 'execName' in a new +// test directory and that directory is added to the path. The content of the +// script is 'EchoQuotedArgs', and the args file is removed using a cleanup +// function. +func PatchExecutableAsEchoArgs(c *gc.C, patcher CleanupPatcher, execName string) { + PatchExecutable(c, patcher, execName, EchoQuotedArgs) + patcher.AddCleanup(func(*gc.C) { + os.Remove(execName + ".out") + }) +} + +// AssertEchoArgs is used to check the args from an execution of a command +// that has been patchec using PatchExecutable containing EchoQuotedArgs. +func AssertEchoArgs(c *gc.C, execName string, args ...string) { + content, err := ioutil.ReadFile(execName + ".out") + c.Assert(err, gc.IsNil) + expected := execName + for _, arg := range args { + expected = fmt.Sprintf("%s %q", expected, arg) + } + expected += "\n" + c.Assert(string(content), gc.Equals, expected) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/cmd_test.go juju-core-1.17.6/src/github.com/juju/testing/cmd_test.go --- juju-core-1.17.4/src/github.com/juju/testing/cmd_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/cmd_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,64 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing_test + +import ( + "os/exec" + + gc "launchpad.net/gocheck" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" +) + +type cmdSuite struct { + testing.CleanupSuite +} + +var _ = gc.Suite(&cmdSuite{}) + +func (s *cmdSuite) TestHookCommandOutput(c *gc.C) { + var CommandOutput = (*exec.Cmd).CombinedOutput + + cmdChan, cleanup := testing.HookCommandOutput(&CommandOutput, []byte{1, 2, 3, 4}, nil) + defer cleanup() + + testCmd := exec.Command("fake-command", "arg1", "arg2") + out, err := CommandOutput(testCmd) + c.Assert(err, gc.IsNil) + cmd := <-cmdChan + c.Assert(out, gc.DeepEquals, []byte{1, 2, 3, 4}) + c.Assert(cmd.Args, gc.DeepEquals, []string{"fake-command", "arg1", "arg2"}) +} + +func (s *cmdSuite) EnsureArgFileRemoved(name string) { + s.AddCleanup(func(c *gc.C) { + c.Assert(name+".out", jc.DoesNotExist) + }) +} + +const testFunc = "test-ouput" + +func (s *cmdSuite) TestPatchExecutableNoArgs(c *gc.C) { + s.EnsureArgFileRemoved(testFunc) + testing.PatchExecutableAsEchoArgs(c, s, testFunc) + output := runCommand(c, testFunc) + c.Assert(output, gc.Equals, testFunc+"\n") + testing.AssertEchoArgs(c, testFunc) +} + +func (s *cmdSuite) TestPatchExecutableWithArgs(c *gc.C) { + s.EnsureArgFileRemoved(testFunc) + testing.PatchExecutableAsEchoArgs(c, s, testFunc) + output := runCommand(c, testFunc, "foo", "bar baz") + c.Assert(output, gc.Equals, testFunc+" \"foo\" \"bar baz\"\n") + testing.AssertEchoArgs(c, testFunc, "foo", "bar baz") +} + +func runCommand(c *gc.C, command string, args ...string) string { + cmd := exec.Command(command, args...) + out, err := cmd.CombinedOutput() + c.Assert(err, gc.IsNil) + return string(out) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/LICENSE juju-core-1.17.6/src/github.com/juju/testing/LICENSE --- juju-core-1.17.4/src/github.com/juju/testing/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/LICENSE 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,185 @@ +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff -Nru juju-core-1.17.4/src/github.com/juju/testing/logging/log.go juju-core-1.17.6/src/github.com/juju/testing/logging/log.go --- juju-core-1.17.4/src/github.com/juju/testing/logging/log.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/logging/log.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,49 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package logging + +import ( + "fmt" + "time" + + "github.com/juju/loggo" + gc "launchpad.net/gocheck" + + "github.com/juju/testing" +) + +// LoggingSuite redirects the juju logger to the test logger +// when embedded in a gocheck suite type. +type LoggingSuite struct { + testing.CleanupSuite +} + +type gocheckWriter struct { + c *gc.C +} + +func (w *gocheckWriter) Write(level loggo.Level, module, filename string, line int, timestamp time.Time, message string) { + // Magic calldepth value... + w.c.Output(3, fmt.Sprintf("%s %s %s", level, module, message)) +} + +func (t *LoggingSuite) SetUpSuite(c *gc.C) { + t.CleanupSuite.SetUpSuite(c) + t.setUp(c) + t.AddSuiteCleanup(func(*gc.C) { + loggo.ResetLoggers() + loggo.ResetWriters() + }) +} + +func (t *LoggingSuite) SetUpTest(c *gc.C) { + t.CleanupSuite.SetUpTest(c) + t.setUp(c) +} + +func (t *LoggingSuite) setUp(c *gc.C) { + loggo.ResetWriters() + loggo.ReplaceDefaultWriter(&gocheckWriter{c}) + loggo.ResetLoggers() +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/logging/log_test.go juju-core-1.17.6/src/github.com/juju/testing/logging/log_test.go --- juju-core-1.17.4/src/github.com/juju/testing/logging/log_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/logging/log_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,57 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package logging_test + +import ( + gc "launchpad.net/gocheck" + + "github.com/juju/loggo" + + "github.com/juju/testing/logging" +) + +var logger = loggo.GetLogger("test") + +type logSuite struct { + logging.LoggingSuite +} + +var _ = gc.Suite(&logSuite{}) + +func (s *logSuite) SetUpSuite(c *gc.C) { + s.LoggingSuite.SetUpSuite(c) + logger.SetLogLevel(loggo.INFO) + logger.Infof("testing-SetUpSuite") + c.Assert(c.GetTestLog(), gc.Matches, ".*INFO test testing-SetUpSuite\n") +} + +func (s *logSuite) TearDownSuite(c *gc.C) { + // Unfortunately there's no way of testing that the + // log output is printed, as the logger is printing + // a previously set up *gc.C. We print a message + // anyway so that we can manually verify it. + logger.Infof("testing-TearDownSuite") +} + +func (s *logSuite) SetUpTest(c *gc.C) { + s.LoggingSuite.SetUpTest(c) + // The SetUpTest resets the logging levels. + logger.SetLogLevel(loggo.INFO) + logger.Infof("testing-SetUpTest") + c.Assert(c.GetTestLog(), gc.Matches, ".*INFO test testing-SetUpTest\n") +} + +func (s *logSuite) TearDownTest(c *gc.C) { + // The same applies here as to TearDownSuite. + logger.Infof("testing-TearDownTest") + s.LoggingSuite.TearDownTest(c) +} + +func (s *logSuite) TestLog(c *gc.C) { + logger.Infof("testing-Test") + c.Assert(c.GetTestLog(), gc.Matches, + ".*INFO test testing-SetUpTest\n"+ + ".*INFO test testing-Test\n", + ) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/logging/package_test.go juju-core-1.17.6/src/github.com/juju/testing/logging/package_test.go --- juju-core-1.17.4/src/github.com/juju/testing/logging/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/logging/package_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package logging_test + +import ( + "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/package_test.go juju-core-1.17.6/src/github.com/juju/testing/package_test.go --- juju-core-1.17.4/src/github.com/juju/testing/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/package_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing_test + +import ( + "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/patch.go juju-core-1.17.6/src/github.com/juju/testing/patch.go --- juju-core-1.17.4/src/github.com/juju/testing/patch.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/patch.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,72 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing + +import ( + "os" + "reflect" + "strings" +) + +// Restorer holds a function that can be used +// to restore some previous state. +type Restorer func() + +// Add returns a Restorer that restores first f1 +// and then f. It is valid to call this on a nil +// Restorer. +func (f Restorer) Add(f1 Restorer) Restorer { + return func() { + f1.Restore() + if f != nil { + f.Restore() + } + } +} + +// Restore restores some previous state. +func (r Restorer) Restore() { + r() +} + +// PatchValue sets the value pointed to by the given destination to the given +// value, and returns a function to restore it to its original value. The +// value must be assignable to the element type of the destination. +func PatchValue(dest, value interface{}) Restorer { + destv := reflect.ValueOf(dest).Elem() + oldv := reflect.New(destv.Type()).Elem() + oldv.Set(destv) + valuev := reflect.ValueOf(value) + if !valuev.IsValid() { + // This isn't quite right when the destination type is not + // nilable, but it's better than the complex alternative. + valuev = reflect.Zero(destv.Type()) + } + destv.Set(valuev) + return func() { + destv.Set(oldv) + } +} + +// PatchEnvironment provides a test a simple way to override a single +// environment variable. A function is returned that will return the +// environment to what it was before. +func PatchEnvironment(name, value string) Restorer { + oldValue := os.Getenv(name) + os.Setenv(name, value) + return func() { + os.Setenv(name, oldValue) + } +} + +// PatchEnvPathPrepend provides a simple way to prepend path to the start of the +// PATH environment variable. Returns a function that restores the environment +// to what it was before. +func PatchEnvPathPrepend(dir string) Restorer { + return PatchEnvironment("PATH", joinPathLists(dir, os.Getenv("PATH"))) +} + +func joinPathLists(paths ...string) string { + return strings.Join(paths, string(os.PathListSeparator)) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/patch_test.go juju-core-1.17.6/src/github.com/juju/testing/patch_test.go --- juju-core-1.17.4/src/github.com/juju/testing/patch_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/patch_test.go 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,100 @@ +// Copyright 2013, 2014 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package testing_test + +import ( + "errors" + "os" + + gc "launchpad.net/gocheck" + + "github.com/juju/testing" +) + +type PatchValueSuite struct{} + +var _ = gc.Suite(&PatchValueSuite{}) + +func (*PatchValueSuite) TestSetInt(c *gc.C) { + i := 99 + restore := testing.PatchValue(&i, 88) + c.Assert(i, gc.Equals, 88) + restore() + c.Assert(i, gc.Equals, 99) +} + +func (*PatchValueSuite) TestSetError(c *gc.C) { + oldErr := errors.New("foo") + newErr := errors.New("bar") + err := oldErr + restore := testing.PatchValue(&err, newErr) + c.Assert(err, gc.Equals, newErr) + restore() + c.Assert(err, gc.Equals, oldErr) +} + +func (*PatchValueSuite) TestSetErrorToNil(c *gc.C) { + oldErr := errors.New("foo") + err := oldErr + restore := testing.PatchValue(&err, nil) + c.Assert(err, gc.Equals, nil) + restore() + c.Assert(err, gc.Equals, oldErr) +} + +func (*PatchValueSuite) TestSetMapToNil(c *gc.C) { + oldMap := map[string]int{"foo": 1234} + m := oldMap + restore := testing.PatchValue(&m, nil) + c.Assert(m, gc.IsNil) + restore() + c.Assert(m, gc.DeepEquals, oldMap) +} + +func (*PatchValueSuite) TestSetPanicsWhenNotAssignable(c *gc.C) { + i := 99 + type otherInt int + c.Assert(func() { testing.PatchValue(&i, otherInt(88)) }, gc.PanicMatches, `reflect\.Set: value of type testing_test\.otherInt is not assignable to type int`) +} + +type PatchEnvironmentSuite struct{} + +var _ = gc.Suite(&PatchEnvironmentSuite{}) + +func (*PatchEnvironmentSuite) TestPatchEnvironment(c *gc.C) { + const envName = "TESTING_PATCH_ENVIRONMENT" + // remember the old value, and set it to something we can check + oldValue := os.Getenv(envName) + os.Setenv(envName, "initial") + restore := testing.PatchEnvironment(envName, "new value") + // Using check to make sure the environment gets set back properly in the test. + c.Check(os.Getenv(envName), gc.Equals, "new value") + restore() + c.Check(os.Getenv(envName), gc.Equals, "initial") + os.Setenv(envName, oldValue) +} + +func (*PatchEnvironmentSuite) TestRestorerAdd(c *gc.C) { + var order []string + first := testing.Restorer(func() { order = append(order, "first") }) + second := testing.Restorer(func() { order = append(order, "second") }) + restore := first.Add(second) + restore() + c.Assert(order, gc.DeepEquals, []string{"second", "first"}) +} + +func (*PatchEnvironmentSuite) TestPatchEnvPathPrepend(c *gc.C) { + oldPath := os.Getenv("PATH") + dir := "/bin/bar" + + // just in case something goes wrong + defer os.Setenv("PATH", oldPath) + + restore := testing.PatchEnvPathPrepend(dir) + + expect := dir + string(os.PathListSeparator) + oldPath + c.Check(os.Getenv("PATH"), gc.Equals, expect) + restore() + c.Check(os.Getenv("PATH"), gc.Equals, oldPath) +} diff -Nru juju-core-1.17.4/src/github.com/juju/testing/README.md juju-core-1.17.6/src/github.com/juju/testing/README.md --- juju-core-1.17.4/src/github.com/juju/testing/README.md 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/github.com/juju/testing/README.md 2014-03-20 12:53:13.000000000 +0000 @@ -0,0 +1,7 @@ +juju/testing +============ + +This package provides additional base test suites to be used with +gocheck. + + diff -Nru juju-core-1.17.4/src/launchpad.net/golxc/golxc.go juju-core-1.17.6/src/launchpad.net/golxc/golxc.go --- juju-core-1.17.4/src/launchpad.net/golxc/golxc.go 2014-02-27 20:20:24.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/golxc/golxc.go 2014-03-20 12:53:38.000000000 +0000 @@ -11,10 +11,15 @@ "fmt" "os" "os/exec" + "path" "strconv" "strings" + + "github.com/juju/loggo" ) +var logger = loggo.GetLogger("golxc") + // Error reports the failure of a LXC command. type Error struct { Name string @@ -65,7 +70,7 @@ Name() string // Create creates a new container based on the given template. - Create(configFile, template string, templateArgs ...string) error + Create(configFile, template string, extraArgs []string, templateArgs []string) error // Start runs the container as a daemon. Start(configFile, consoleFile string) error @@ -74,7 +79,7 @@ Stop() error // Clone creates a copy of the container, giving the copy the specified name. - Clone(name string) (Container, error) + Clone(name string, extraArgs []string, templateArgs []string) (Container, error) // Freeze freezes all the container's processes. Freeze() error @@ -127,18 +132,26 @@ return &containerFactory{} } +const DefaultLXCDir = "/var/lib/lxc" + +var ContainerDir = DefaultLXCDir + type container struct { name string logFile string logLevel LogLevel + // Newer LXC libraries can have containers in non-default locations. The + // containerDir is the directory that is the 'home' of this container. + containerDir string } type containerFactory struct{} func (*containerFactory) New(name string) Container { return &container{ - name: name, - logLevel: LogWarning, + name: name, + logLevel: LogWarning, + containerDir: ContainerDir, } } @@ -179,7 +192,7 @@ } // Create creates a new container based on the given template. -func (c *container) Create(configFile, template string, templateArgs ...string) error { +func (c *container) Create(configFile, template string, extraArgs []string, templateArgs []string) error { if c.IsConstructed() { return fmt.Errorf("container %q is already created", c.Name()) } @@ -190,8 +203,12 @@ if configFile != "" { args = append(args, "-f", configFile) } - args = append(args, "--") + if len(extraArgs) != 0 { + args = append(args, extraArgs...) + } if len(templateArgs) != 0 { + // Must be done in two steps due to current language implementation details. + args = append(args, "--") args = append(args, templateArgs...) } _, err := run("lxc-create", args...) @@ -223,7 +240,13 @@ if err != nil { return err } - return c.Wait(StateRunning) + if err := c.Wait(StateRunning, StateStopped); err != nil { + return err + } + if !c.IsRunning() { + return fmt.Errorf("container failed to start") + } + return nil } // Stop terminates the running container. @@ -231,6 +254,10 @@ if !c.IsConstructed() { return fmt.Errorf("container %q is not yet created", c.name) } + // If the container is not running, we are done. + if !c.IsRunning() { + return nil + } args := []string{ "-n", c.name, } @@ -245,12 +272,17 @@ } // Clone creates a copy of the container, it gets the given name. -func (c *container) Clone(name string) (Container, error) { +func (c *container) Clone(name string, extraArgs []string, templateArgs []string) (Container, error) { if !c.IsConstructed() { return nil, fmt.Errorf("container %q is not yet created", c.name) } + if c.IsRunning() { + return nil, fmt.Errorf("cannot clone a running container") + } cc := &container{ - name: name, + name: name, + logLevel: c.logLevel, + containerDir: c.containerDir, } if cc.IsConstructed() { return cc, nil @@ -259,6 +291,14 @@ "-o", c.name, "-n", name, } + if len(extraArgs) != 0 { + args = append(args, extraArgs...) + } + if len(templateArgs) != 0 { + // Must be done in two steps due to current language implementation details. + args = append(args, "--") + args = append(args, templateArgs...) + } _, err := run("lxc-clone", args...) if err != nil { return nil, err @@ -384,27 +424,32 @@ // containerHome returns the name of the container directory. func (c *container) containerHome() string { - return "/var/lib/lxc/" + c.name + return path.Join(c.containerDir, c.name) } // rootfs returns the name of the directory containing the // root filesystem of the container. func (c *container) rootfs() string { - return c.containerHome() + "/rootfs/" + return path.Join(c.containerHome(), "rootfs") } // run executes the passed command and returns the out. func run(name string, args ...string) (string, error) { + logger := loggo.GetLogger(fmt.Sprintf("golxc.run.%s", name)) + logger.Tracef("run: %s %v", name, args) cmd := exec.Command(name, args...) // LXC tools do not use stdout and stderr in a predictable // way; based on experimentation, the most convenient // solution is to combine them and leave the client to // determine sanity as best it can. out, err := cmd.CombinedOutput() + result := string(out) if err != nil { + logger.Tracef("run failed output: %s", result) return "", runError(name, err, out) } - return string(out), nil + logger.Tracef("run successful output: %s", result) + return result, nil } // runError creates an error if run fails. diff -Nru juju-core-1.17.4/src/launchpad.net/golxc/golxc_test.go juju-core-1.17.6/src/launchpad.net/golxc/golxc_test.go --- juju-core-1.17.4/src/launchpad.net/golxc/golxc_test.go 2014-02-27 20:20:24.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/golxc/golxc_test.go 2014-03-20 12:53:38.000000000 +0000 @@ -1,18 +1,22 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see COPYING and COPYING.LESSER file for details. + package golxc_test import ( "io/ioutil" - . "launchpad.net/gocheck" "os" "os/user" "path/filepath" - "testing" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + "github.com/juju/testing/logging" + . "launchpad.net/gocheck" "launchpad.net/golxc" ) -func Test(t *testing.T) { TestingT(t) } - var lxcfile = `# MIRROR to be used by ubuntu template at container creation: # Leaving it undefined is fine #MIRROR="http://archive.ubuntu.com/ubuntu" @@ -139,6 +143,15 @@ } } +func (s *LXCSuite) createContainer(c *C) golxc.Container { + container := s.factory.New("golxc") + c.Assert(container.IsConstructed(), Equals, false) + err := container.Create("", "ubuntu", nil, nil) + c.Assert(err, IsNil) + c.Assert(container.IsConstructed(), Equals, true) + return container +} + func (s *LXCSuite) TestCreateDestroy(c *C) { // Test clean creation and destroying of a container. lc := s.factory.New("golxc") @@ -146,7 +159,7 @@ home := golxc.ContainerHome(lc) _, err := os.Stat(home) c.Assert(err, ErrorMatches, "stat .*: no such file or directory") - err = lc.Create("", "ubuntu") + err = lc.Create("", "ubuntu", nil, nil) c.Assert(err, IsNil) c.Assert(lc.IsConstructed(), Equals, true) defer func() { @@ -162,16 +175,13 @@ func (s *LXCSuite) TestCreateTwice(c *C) { // Test that a container cannot be created twice. - lc1 := s.factory.New("golxc") - c.Assert(lc1.IsConstructed(), Equals, false) - err := lc1.Create("", "ubuntu") - c.Assert(err, IsNil) + lc1 := s.createContainer(c) c.Assert(lc1.IsConstructed(), Equals, true) defer func() { c.Assert(lc1.Destroy(), IsNil) }() lc2 := s.factory.New("golxc") - err = lc2.Create("", "ubuntu") + err := lc2.Create("", "ubuntu", nil, nil) c.Assert(err, ErrorMatches, "container .* is already created") } @@ -180,8 +190,8 @@ // case of an illegal template. lc := s.factory.New("golxc") c.Assert(lc.IsConstructed(), Equals, false) - err := lc.Create("", "name-of-a-not-existing-template-for-golxc") - c.Assert(err, ErrorMatches, `error executing "lxc-create": No config file specified, .*`) + err := lc.Create("", "name-of-a-not-existing-template-for-golxc", nil, nil) + c.Assert(err, ErrorMatches, `error executing "lxc-create": .*bad template.*`) c.Assert(lc.IsConstructed(), Equals, false) } @@ -208,10 +218,7 @@ oldLen := len(lcs) c.Assert(err, IsNil) c.Assert(oldLen >= 0, Equals, true) - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) - c.Assert(lc.IsConstructed(), Equals, true) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -223,16 +230,13 @@ func (s *LXCSuite) TestClone(c *C) { // Test the cloning of an existing container. - lc1 := s.factory.New("golxc") - c.Assert(lc1.IsConstructed(), Equals, false) - c.Assert(lc1.Create("", "ubuntu"), IsNil) - c.Assert(lc1.IsConstructed(), Equals, true) + lc1 := s.createContainer(c) defer func() { c.Assert(lc1.Destroy(), IsNil) }() lcs, _ := s.factory.List() oldLen := len(lcs) - lc2, err := lc1.Clone("golxcclone") + lc2, err := lc1.Clone("golxcclone", nil, nil) c.Assert(err, IsNil) c.Assert(lc2.IsConstructed(), Equals, true) defer func() { @@ -249,15 +253,13 @@ // Test the cloning of a non-existing container. lc := s.factory.New("golxc") c.Assert(lc.IsConstructed(), Equals, false) - _, err := lc.Clone("golxcclone") + _, err := lc.Clone("golxcclone", nil, nil) c.Assert(err, ErrorMatches, "container .* is not yet created") } func (s *LXCSuite) TestStartStop(c *C) { // Test starting and stopping a container. - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -276,9 +278,7 @@ func (s *LXCSuite) TestStopNotRunning(c *C) { // Test that a not running container can't be stopped. - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -293,7 +293,7 @@ c.Assert(lc.Wait(), ErrorMatches, "no states specified") c.Assert(lc.Wait(golxc.StateStopped), IsNil) c.Assert(lc.Wait(golxc.StateStopped, golxc.StateRunning), IsNil) - c.Assert(lc.Create("", "ubuntu"), IsNil) + c.Assert(lc.Create("", "ubuntu", nil, nil), IsNil) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -305,9 +305,7 @@ func (s *LXCSuite) TestFreezeUnfreeze(c *C) { // Test the freezing and unfreezing of a started container. - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -324,9 +322,7 @@ func (s *LXCSuite) TestFreezeNotStarted(c *C) { // Test that a not running container can't be frozen. - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -349,9 +345,7 @@ func (s *LXCSuite) TestUnfreezeNotFrozen(c *C) { // Test that a running container can't be unfrozen. - lc := s.factory.New("golxc") - c.Assert(lc.IsConstructed(), Equals, false) - c.Assert(lc.Create("", "ubuntu"), IsNil) + lc := s.createContainer(c) defer func() { c.Assert(lc.Destroy(), IsNil) }() @@ -361,3 +355,60 @@ }() c.Assert(lc.Unfreeze(), ErrorMatches, "container .* is not frozen") } + +type commandArgs struct { + logging.LoggingSuite +} + +var _ = Suite(&commandArgs{}) + +func (s *commandArgs) TestCreateArgs(c *C) { + s.PatchValue(&golxc.ContainerDir, c.MkDir()) + testing.PatchExecutableAsEchoArgs(c, s, "lxc-create") + + factory := golxc.Factory() + container := factory.New("test") + err := container.Create( + "config-file", "template", + []string{"extra-1", "extra-2"}, + []string{"template-1", "template-2"}, + ) + c.Assert(err, IsNil) + testing.AssertEchoArgs( + c, "lxc-create", + "-n", "test", + "-t", "template", + "-f", "config-file", + "extra-1", "extra-2", + "--", "template-1", "template-2") +} + +func (s *commandArgs) TestCloneArgs(c *C) { + dir := c.MkDir() + s.PatchValue(&golxc.ContainerDir, dir) + // Patch lxc-info too as clone checks to see if it is running. + testing.PatchExecutableAsEchoArgs(c, s, "lxc-info") + testing.PatchExecutableAsEchoArgs(c, s, "lxc-clone") + + factory := golxc.Factory() + container := factory.New("test") + // Make the rootfs for the "test" container so it thinks it is created. + rootfs := filepath.Join(dir, "test", "rootfs") + err := os.MkdirAll(rootfs, 0755) + c.Assert(err, IsNil) + c.Assert(rootfs, jc.IsDirectory) + c.Assert(container.IsConstructed(), jc.IsTrue) + clone, err := container.Clone( + "name", + []string{"extra-1", "extra-2"}, + []string{"template-1", "template-2"}, + ) + c.Assert(err, IsNil) + testing.AssertEchoArgs( + c, "lxc-clone", + "-o", "test", + "-n", "name", + "extra-1", "extra-2", + "--", "template-1", "template-2") + c.Assert(clone.Name(), Equals, "name") +} diff -Nru juju-core-1.17.4/src/launchpad.net/golxc/package_test.go juju-core-1.17.6/src/launchpad.net/golxc/package_test.go --- juju-core-1.17.4/src/launchpad.net/golxc/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/golxc/package_test.go 2014-03-20 12:53:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see COPYING and COPYING.LESSER file for details. + +package golxc_test + +import ( + "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/agent.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/agent.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/agent.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/agent.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,13 +4,18 @@ package agent import ( + "bytes" "fmt" + "io/ioutil" + "os" "path" + "path/filepath" "regexp" + "strings" "sync" "github.com/errgo/errgo" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" @@ -22,6 +27,14 @@ var logger = loggo.GetLogger("juju.agent") +// DefaultLogDir defines the default log directory for juju agents. +// It's defined as a variable so it could be overridden in tests. +var DefaultLogDir = "/var/log/juju" + +// DefaultDataDir defines the default data directory for juju agents. +// It's defined as a variable so it could be overridden in tests. +var DefaultDataDir = "/var/lib/juju" + const ( LxcBridge = "LXC_BRIDGE" ProviderType = "PROVIDER_TYPE" @@ -31,7 +44,6 @@ StorageAddr = "STORAGE_ADDR" AgentServiceName = "AGENT_SERVICE_NAME" MongoServiceName = "MONGO_SERVICE_NAME" - BootstrapJobs = "BOOTSTRAP_JOBS" ) // The Config interface is the sole way that the agent gets access to the @@ -49,6 +61,13 @@ // containing the configuration files. DataDir() string + // LogDir returns the log directory. All logs from all agents on + // the machine are written to this directory. + LogDir() string + + // Jobs returns a list of MachineJobs that need to run. + Jobs() []params.MachineJob + // Tag returns the tag of the entity on whose behalf the state connection // will be made. Tag() string @@ -106,22 +125,84 @@ StateInitializer } +// MigrateConfigParams holds agent config values to change in a +// MigrateConfig call. Empty fields will be ignored. DeleteValues +// specifies a list of keys to delete. +type MigrateConfigParams struct { + DataDir string + LogDir string + Jobs []params.MachineJob + DeleteValues []string + Values map[string]string +} + +// MigrateConfig takes an existing agent config and applies the given +// newParams selectively. Only non-empty fields in newParams are used +// to change existing config settings. All changes are written +// atomically. UpgradedToVersion cannot be changed here, because +// MigrateConfig is most likely called during an upgrade, so it will be +// changed at the end of the upgrade anyway, if successful. +func MigrateConfig(currentConfig Config, newParams MigrateConfigParams) error { + configMutex.Lock() + defer configMutex.Unlock() + config := currentConfig.(*configInternal) + + if newParams.DataDir != "" { + config.dataDir = newParams.DataDir + } + if newParams.LogDir != "" { + config.logDir = newParams.LogDir + } + if len(newParams.Jobs) > 0 { + config.jobs = make([]params.MachineJob, len(newParams.Jobs)) + copy(config.jobs, newParams.Jobs) + } + for _, key := range newParams.DeleteValues { + delete(config.values, key) + } + for key, value := range newParams.Values { + if config.values == nil { + config.values = make(map[string]string) + } + config.values[key] = value + } + if err := config.check(); err != nil { + return fmt.Errorf("migrated agent config is invalid: %v", err) + } + oldConfigFile := config.configFilePath + config.configFilePath = ConfigPath(config.dataDir, config.tag) + if err := config.write(); err != nil { + return fmt.Errorf("cannot migrate agent config: %v", err) + } + if oldConfigFile != config.configFilePath && oldConfigFile != "" { + err := os.Remove(oldConfigFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("cannot remove old agent config %q: %v", oldConfigFile, err) + } + } + return nil +} + // Ensure that the configInternal struct implements the Config interface. var _ Config = (*configInternal)(nil) -// The configMutex should be locked before any writing to disk during the -// write commands, and unlocked when the writing is complete. This process -// wide lock should stop any unintended concurrent writes. This may happen -// when multiple go-routines may be adding things to the agent config, and -// wanting to persist them to disk. To ensure that the correct data is written -// to disk, the mutex should be locked prior to generating any disk state. -// This way calls that might get interleaved would always write the most -// recent state to disk. Since we have different agent configs for each -// agent, and there is only one process for each agent, a simple mutex is -// enough for concurrency. The mutex should also be locked around any access -// to mutable values, either setting or getting. The only mutable value is -// the values map. Retrieving and setting values here are protected by the -// mutex. New mutating methods should also be synchronized using this mutex. +// The configMutex should be locked before any writing to disk during +// the write commands, and unlocked when the writing is complete. This +// process wide lock should stop any unintended concurrent writes. +// This may happen when multiple go-routines may be adding things to +// the agent config, and wanting to persist them to disk. To ensure +// that the correct data is written to disk, the mutex should be +// locked prior to generating any disk state. This way calls that +// might get interleaved would always write the most recent state to +// disk. Since we have different agent configs for each agent, and +// there is only one process for each agent, a simple mutex is enough +// for concurrency. The mutex should also be locked around any access +// to mutable values, either setting or getting. The only mutable +// value is the values map. Retrieving and setting values here are +// protected by the mutex. New mutating methods should also be +// synchronized using this mutex. Config is essentially a singleton +// implementation, having a non-constructable-in-a-normal-way backing +// type configInternal. var configMutex sync.Mutex type connectionDetails struct { @@ -130,10 +211,13 @@ } type configInternal struct { + configFilePath string dataDir string + logDir string tag string - upgradedToVersion version.Number nonce string + jobs []params.MachineJob + upgradedToVersion version.Number caCert []byte stateDetails *connectionDetails apiDetails *connectionDetails @@ -146,8 +230,10 @@ type AgentConfigParams struct { DataDir string - Tag string + LogDir string + Jobs []params.MachineJob UpgradedToVersion version.Number + Tag string Password string Nonce string StateAddresses []string @@ -162,6 +248,10 @@ if configParams.DataDir == "" { return nil, errgo.Trace(requiredError("data directory")) } + logDir := DefaultLogDir + if configParams.LogDir != "" { + logDir = configParams.LogDir + } if configParams.Tag == "" { return nil, errgo.Trace(requiredError("entity tag")) } @@ -171,15 +261,17 @@ if configParams.Password == "" { return nil, errgo.Trace(requiredError("password")) } - if configParams.CACert == nil { + if len(configParams.CACert) == 0 { return nil, errgo.Trace(requiredError("CA certificate")) } // Note that the password parts of the state and api information are // blank. This is by design. config := &configInternal{ + logDir: logDir, dataDir: configParams.DataDir, - tag: configParams.Tag, + jobs: configParams.Jobs, upgradedToVersion: configParams.UpgradedToVersion, + tag: configParams.Tag, nonce: configParams.Nonce, caCert: configParams.CACert, oldPassword: configParams.Password, @@ -201,6 +293,7 @@ if config.values == nil { config.values = make(map[string]string) } + config.configFilePath = ConfigPath(config.dataDir, config.tag) return config, nil } @@ -234,46 +327,74 @@ // Dir returns the agent-specific data directory. func Dir(dataDir, agentName string) string { + // Note: must use path, not filepath, as this + // function is used by the client on Windows. return path.Join(dataDir, "agents", agentName) } -// ReadConf reads configuration data for the given -// entity from the given data directory. -func ReadConf(dataDir, tag string) (Config, error) { +// ConfigPath returns the full path to the agent config file. +// NOTE: Delete this once all agents accept --config instead +// of --data-dir - it won't be needed anymore. +func ConfigPath(dataDir, agentName string) string { + return filepath.Join(Dir(dataDir, agentName), agentConfigFilename) +} + +// ReadConf reads configuration data from the given location. +func ReadConf(configFilePath string) (Config, error) { // Even though the ReadConf is done at the start of the agent loading, and // that this should not be called more than once by an agent, I feel that // not locking the mutex that is used to protect writes is wrong. configMutex.Lock() defer configMutex.Unlock() - dir := Dir(dataDir, tag) - format, err := readFormat(dir) + var ( + format formatter + config *configInternal + ) + configData, err := ioutil.ReadFile(configFilePath) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot read agent config %q: %v", configFilePath, err) } - logger.Debugf("Reading agent config, format: %s", format) - formatter, err := newFormatter(format) - if err != nil { - return nil, err + + // Try to read the legacy format file. + dir := filepath.Dir(configFilePath) + legacyFormatPath := filepath.Join(dir, legacyFormatFilename) + formatBytes, err := ioutil.ReadFile(legacyFormatPath) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("cannot read format file: %v", err) + } + formatData := string(formatBytes) + if err == nil { + // It exists, so unmarshal with a legacy formatter. + // Drop the format prefix to leave the version only. + if !strings.HasPrefix(formatData, legacyFormatPrefix) { + return nil, fmt.Errorf("malformed agent config format %q", formatData) + } + format, err = getFormatter(strings.TrimPrefix(formatData, legacyFormatPrefix)) + if err != nil { + return nil, err + } + config, err = format.unmarshal(configData) + } else { + // Does not exist, just parse the data. + format, config, err = parseConfigData(configData) } - config, err := formatter.read(dir) if err != nil { return nil, err } - config.dataDir = dataDir - if err := config.check(); err != nil { - return nil, err - } - + logger.Debugf("read agent config, format %q", format.version()) + config.configFilePath = configFilePath if format != currentFormat { - // Migrate the config to the new format. - currentFormatter.migrate(config) - // Write the content out in the new format. - if err := currentFormatter.write(config); err != nil { - logger.Errorf("cannot write the agent config in format %s: %v", currentFormat, err) - return nil, err + // Migrate from a legacy format to the new one. + err := config.write() + if err != nil { + return nil, fmt.Errorf("cannot migrate %s agent config to %s: %v", format.version(), currentFormat.version(), err) + } + logger.Debugf("migrated agent config from %s to %s", format.version(), currentFormat.version()) + err = os.Remove(legacyFormatPath) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("cannot remove legacy format file %q: %v", legacyFormatPath, err) } } - return config, nil } @@ -289,6 +410,14 @@ return c.dataDir } +func (c *configInternal) LogDir() string { + return c.logDir +} + +func (c *configInternal) Jobs() []params.MachineJob { + return c.jobs +} + func (c *configInternal) Nonce() string { return c.nonce } @@ -392,24 +521,51 @@ other.apiDetails = &apiDetails } logger.Debugf("writing configuration file") - if err := other.Write(); err != nil { + if err := other.write(); err != nil { return "", err } *c = other return newPassword, nil } +func (c *configInternal) fileContents() ([]byte, error) { + data, err := currentFormat.marshal(c) + if err != nil { + return nil, err + } + var buf bytes.Buffer + fmt.Fprintf(&buf, "%s%s\n", formatPrefix, currentFormat.version()) + buf.Write(data) + return buf.Bytes(), nil +} + +// write is the internal implementation of c.Write(). +func (c *configInternal) write() error { + data, err := c.fileContents() + if err != nil { + return err + } + // Make sure the config dir gets created. + configDir := filepath.Dir(c.configFilePath) + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("cannot create agent config dir %q: %v", configDir, err) + } + return utils.AtomicWriteFile(c.configFilePath, data, 0600) +} + func (c *configInternal) Write() error { // Lock is taken prior to generating any content to write. configMutex.Lock() defer configMutex.Unlock() - return currentFormatter.write(c) + return c.write() } func (c *configInternal) WriteUpgradedToVersion(newVersion version.Number) error { + configMutex.Lock() + defer configMutex.Unlock() originalVersion := c.upgradedToVersion c.upgradedToVersion = newVersion - err := c.Write() + err := c.write() if err != nil { // We don't want to retain the new version if there's been an error writing the file. c.upgradedToVersion = originalVersion @@ -418,10 +574,18 @@ } func (c *configInternal) WriteCommands() ([]string, error) { - return currentFormatter.writeCommands(c) + data, err := c.fileContents() + if err != nil { + return nil, err + } + commands := []string{"mkdir -p " + utils.ShQuote(c.Dir())} + commands = append(commands, writeFileCommands(c.File(agentConfigFilename), data, 0600)...) + return commands, nil } func (c *configInternal) OpenAPI(dialOpts api.DialOpts) (st *api.State, newPassword string, err error) { + configMutex.Lock() + defer configMutex.Unlock() info := api.Info{ Addrs: c.apiDetails.addresses, Password: c.apiDetails.password, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/agent_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/agent_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/agent_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/agent_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,9 +4,13 @@ package agent_test import ( + "reflect" + + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -18,14 +22,15 @@ var _ = gc.Suite(&suite{}) var agentConfigTests = []struct { - about string - params agent.AgentConfigParams - checkErr string + about string + params agent.AgentConfigParams + checkErr string + inspectConfig func(*gc.C, agent.Config) }{{ about: "missing data directory", checkErr: "data directory not found in configuration", }, { - about: "missing tag directory", + about: "missing tag", params: agent.AgentConfigParams{ DataDir: "/data/dir", }, @@ -129,6 +134,21 @@ APIAddresses: []string{"localhost:1235"}, Nonce: "a nonce", }, +}, { + about: "missing logDir sets default", + params: agent.AgentConfigParams{ + DataDir: "/data/dir", + Tag: "omg", + Password: "sekrit", + UpgradedToVersion: version.Current.Number, + CACert: []byte("ca cert"), + StateAddresses: []string{"localhost:1234"}, + APIAddresses: []string{"localhost:1235"}, + Nonce: "a nonce", + }, + inspectConfig: func(c *gc.C, cfg agent.Config) { + c.Check(cfg.LogDir(), gc.Equals, agent.DefaultLogDir) + }, }} func (*suite) TestNewAgentConfig(c *gc.C) { @@ -144,11 +164,139 @@ } } +func (*suite) TestMigrateConfig(c *gc.C) { + initialParams := agent.AgentConfigParams{ + DataDir: c.MkDir(), + LogDir: c.MkDir(), + Tag: "omg", + Nonce: "nonce", + Password: "secret", + UpgradedToVersion: version.MustParse("1.16.5"), + Jobs: []params.MachineJob{ + params.JobManageEnviron, + params.JobHostUnits, + }, + CACert: []byte("ca cert"), + StateAddresses: []string{"localhost:1234"}, + APIAddresses: []string{"localhost:4321"}, + Values: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + } + + migrateTests := []struct { + comment string + fields []string + newParams agent.MigrateConfigParams + expectValues map[string]string + expectErr string + }{{ + comment: "nothing to change", + fields: nil, + newParams: agent.MigrateConfigParams{}, + }, { + fields: []string{"DataDir"}, + newParams: agent.MigrateConfigParams{ + DataDir: c.MkDir(), + }, + }, { + fields: []string{"DataDir", "LogDir"}, + newParams: agent.MigrateConfigParams{ + DataDir: c.MkDir(), + LogDir: c.MkDir(), + }, + }, { + fields: []string{"Jobs"}, + newParams: agent.MigrateConfigParams{ + Jobs: []params.MachineJob{params.JobHostUnits}, + }, + }, { + comment: "invalid/immutable field specified", + fields: []string{"InvalidField"}, + newParams: agent.MigrateConfigParams{}, + expectErr: `unknown field "InvalidField"`, + }, { + comment: "Values can be added, changed or removed", + fields: []string{"Values", "DeleteValues"}, + newParams: agent.MigrateConfigParams{ + DeleteValues: []string{"key2", "key3"}, // delete + Values: map[string]string{ + "key1": "new value1", // change + "new key3": "value3", // add + "empty": "", // add empty val + }, + }, + expectValues: map[string]string{ + "key1": "new value1", + "new key3": "value3", + "empty": "", + }, + }} + for i, test := range migrateTests { + summary := "migrate fields" + if test.comment != "" { + summary += " (" + test.comment + ") " + } + c.Logf("test %d: %s %v", i, summary, test.fields) + + initialConfig, err := agent.NewAgentConfig(initialParams) + c.Check(err, gc.IsNil) + + newConfig, err := agent.NewAgentConfig(initialParams) + c.Check(err, gc.IsNil) + + c.Check(initialConfig.Write(), gc.IsNil) + c.Check(agent.ConfigFileExists(initialConfig), jc.IsTrue) + + err = agent.MigrateConfig(newConfig, test.newParams) + c.Check(err, gc.IsNil) + c.Check(agent.ConfigFileExists(newConfig), jc.IsTrue) + if test.newParams.DataDir != "" { + // If we're changing where the new config is saved, + // we can verify the old config got removed. + c.Check(agent.ConfigFileExists(initialConfig), jc.IsFalse) + } + + // Make sure we can read it back successfully and it + // matches what we wrote. + configPath := agent.ConfigPath(newConfig.DataDir(), newConfig.Tag()) + readConfig, err := agent.ReadConf(configPath) + c.Check(err, gc.IsNil) + c.Check(newConfig, jc.DeepEquals, readConfig) + + // Make sure only the specified fields were changed and + // the rest matches. + for _, field := range test.fields { + switch field { + case "Values": + err = agent.PatchConfig(initialConfig, field, test.expectValues) + c.Check(err, gc.IsNil) + case "DeleteValues": + err = agent.PatchConfig(initialConfig, field, test.newParams.DeleteValues) + c.Check(err, gc.IsNil) + default: + value := reflect.ValueOf(test.newParams).FieldByName(field) + if value.IsValid() && test.expectErr == "" { + err = agent.PatchConfig(initialConfig, field, value.Interface()) + c.Check(err, gc.IsNil) + } else { + err = agent.PatchConfig(initialConfig, field, value) + c.Check(err, gc.ErrorMatches, test.expectErr) + } + } + } + c.Check(newConfig, jc.DeepEquals, initialConfig) + } +} + func (*suite) TestNewStateMachineConfig(c *gc.C) { type testStruct struct { - about string - params agent.StateMachineConfigParams - checkErr string + about string + params agent.StateMachineConfigParams + checkErr string + inspectConfig func(*gc.C, agent.Config) } var tests = []testStruct{{ about: "missing state server cert", @@ -175,9 +323,12 @@ for i, test := range tests { c.Logf("%v: %s", i, test.about) - _, err := agent.NewStateMachineConfig(test.params) + cfg, err := agent.NewStateMachineConfig(test.params) if test.checkErr == "" { c.Assert(err, gc.IsNil) + if test.inspectConfig != nil { + test.inspectConfig(c, cfg) + } } else { c.Assert(err, gc.ErrorMatches, test.checkErr) } @@ -218,26 +369,17 @@ c.Assert(newValue, gc.DeepEquals, []string{"localhost:1235"}) } -func assertConfigEqual(c *gc.C, c1, c2 agent.Config) { - // Since we can't directly poke the internals, we'll use the WriteCommands - // method. - conf1Commands, err := c1.WriteCommands() - c.Assert(err, gc.IsNil) - conf2Commands, err := c2.WriteCommands() - c.Assert(err, gc.IsNil) - c.Assert(conf1Commands, gc.DeepEquals, conf2Commands) -} - func (*suite) TestWriteAndRead(c *gc.C) { testParams := attributeParams testParams.DataDir = c.MkDir() + testParams.LogDir = c.MkDir() conf, err := agent.NewAgentConfig(testParams) c.Assert(err, gc.IsNil) c.Assert(conf.Write(), gc.IsNil) - reread, err := agent.ReadConf(conf.DataDir(), conf.Tag()) + reread, err := agent.ReadConf(agent.ConfigPath(conf.DataDir(), conf.Tag())) c.Assert(err, gc.IsNil) - assertConfigEqual(c, conf, reread) + c.Assert(reread, jc.DeepEquals, conf) } func (*suite) TestWriteNewPassword(c *gc.C) { @@ -284,7 +426,7 @@ newPass, err := agent.WriteNewPassword(conf) c.Assert(err, gc.IsNil) // Show that the password is saved. - reread, err := agent.ReadConf(conf.DataDir(), conf.Tag()) + reread, err := agent.ReadConf(agent.ConfigPath(conf.DataDir(), conf.Tag())) c.Assert(agent.Password(conf), gc.Equals, agent.Password(reread)) c.Assert(newPass, gc.Equals, agent.Password(conf)) } @@ -303,8 +445,8 @@ c.Assert(conf.UpgradedToVersion(), gc.DeepEquals, newVersion) // Show that the upgradedToVersion is saved. - reread, err := agent.ReadConf(conf.DataDir(), conf.Tag()) - assertConfigEqual(c, conf, reread) + reread, err := agent.ReadConf(agent.ConfigPath(conf.DataDir(), conf.Tag())) + c.Assert(reread, jc.DeepEquals, conf) } // Actual opening of state and api requires a lot more boiler plate to make diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/bootstrap.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/bootstrap.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/bootstrap.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,6 @@ package agent import ( - "encoding/json" "fmt" "launchpad.net/juju-core/constraints" @@ -12,6 +11,7 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/version" ) @@ -32,21 +32,6 @@ InitializeState(envCfg *config.Config, machineCfg BootstrapMachineConfig, timeout state.DialOpts, policy state.Policy) (*state.State, *state.Machine, error) } -// MarshalBootstrapJobs may be used to marshal a set of -// machine jobs for the bootstrap agent, to be added -// into the agent configuration. -func MarshalBootstrapJobs(jobs ...state.MachineJob) (string, error) { - b, err := json.Marshal(jobs) - return string(b), err -} - -// UnmarshalBootstrapJobs unmarshals a set of machine -// jobs marshalled with MarshalBootstrapJobs. -func UnmarshalBootstrapJobs(s string) (jobs []state.MachineJob, err error) { - err = json.Unmarshal([]byte(s), &jobs) - return jobs, err -} - // BootstrapMachineConfig holds configuration information // to attach to the bootstrap machine. type BootstrapMachineConfig struct { @@ -55,7 +40,7 @@ Constraints constraints.Value // Jobs holds the jobs that the machine agent will run. - Jobs []state.MachineJob + Jobs []params.MachineJob // InstanceId holds the instance id of the bootstrap machine. InstanceId instance.Id @@ -138,13 +123,21 @@ logger.Infof("initialising bootstrap machine with config: %+v", cfg) + jobs := make([]state.MachineJob, len(cfg.Jobs)) + for i, job := range cfg.Jobs { + machineJob, err := state.MachineJobFromParams(job) + if err != nil { + return nil, fmt.Errorf("invalid bootstrap machine job %q: %v", job, err) + } + jobs[i] = machineJob + } m, err := st.AddOneMachine(state.MachineTemplate{ Series: version.Current.Series, Nonce: state.BootstrapNonce, Constraints: cfg.Constraints, InstanceId: cfg.InstanceId, HardwareCharacteristics: cfg.Characteristics, - Jobs: cfg.Jobs, + Jobs: jobs, }) if err != nil { return nil, fmt.Errorf("cannot create bootstrap machine in state: %v", err) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package agent_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -12,8 +13,8 @@ "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -63,7 +64,7 @@ expectHW := instance.MustParseHardware("mem=2048M") mcfg := agent.BootstrapMachineConfig{ Constraints: expectConstraints, - Jobs: []state.MachineJob{state.JobHostUnits}, + Jobs: []params.MachineJob{params.JobHostUnits}, InstanceId: "i-bootstrap", Characteristics: expectHW, } @@ -103,7 +104,7 @@ // Check that the machine agent's config has been written // and that we can use it to connect to the state. - newCfg, err := agent.ReadConf(dataDir, "machine-0") + newCfg, err := agent.ReadConf(agent.ConfigPath(dataDir, "machine-0")) c.Assert(err, gc.IsNil) c.Assert(newCfg.Tag(), gc.Equals, "machine-0") c.Assert(agent.Password(newCfg), gc.Not(gc.Equals), pwHash) @@ -129,7 +130,7 @@ expectHW := instance.MustParseHardware("mem=2048M") mcfg := agent.BootstrapMachineConfig{ Constraints: expectConstraints, - Jobs: []state.MachineJob{state.JobHostUnits}, + Jobs: []params.MachineJob{params.JobHostUnits}, InstanceId: "i-bootstrap", Characteristics: expectHW, } @@ -165,19 +166,3 @@ _, err = st.Machine("0") c.Assert(err, gc.IsNil) } - -func (s *bootstrapSuite) TestMarshalUnmarshalBootstrapJobs(c *gc.C) { - jobs := [][]state.MachineJob{ - {}, - {state.JobHostUnits}, - {state.JobManageEnviron}, - {state.JobHostUnits, state.JobManageEnviron}, - } - for _, jobs := range jobs { - marshalled, err := agent.MarshalBootstrapJobs(jobs...) - c.Assert(err, gc.IsNil) - unmarshalled, err := agent.UnmarshalBootstrapJobs(marshalled) - c.Assert(err, gc.IsNil) - c.Assert(unmarshalled, gc.DeepEquals, jobs) - } -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -3,6 +3,13 @@ package agent +import ( + "fmt" + "os" + + "launchpad.net/juju-core/state/api/params" +) + func Password(config Config) string { c := config.(*configInternal) if c.stateDetails == nil { @@ -16,3 +23,36 @@ func WriteNewPassword(cfg Config) (string, error) { return cfg.(*configInternal).writeNewPassword() } + +func PatchConfig(config Config, fieldName string, value interface{}) error { + conf := config.(*configInternal) + switch fieldName { + case "DataDir": + conf.dataDir = value.(string) + case "LogDir": + conf.logDir = value.(string) + case "Jobs": + conf.jobs = value.([]params.MachineJob)[:] + case "DeleteValues": + for _, key := range value.([]string) { + delete(conf.values, key) + } + case "Values": + for key, val := range value.(map[string]string) { + if conf.values == nil { + conf.values = make(map[string]string) + } + conf.values[key] = val + } + default: + return fmt.Errorf("unknown field %q", fieldName) + } + conf.configFilePath = ConfigPath(conf.dataDir, conf.tag) + return nil +} + +func ConfigFileExists(config Config) bool { + conf := config.(*configInternal) + _, err := os.Lstat(conf.configFilePath) + return err == nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.12.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.12.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.12.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.12.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,177 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package agent - -import ( - "fmt" - "io/ioutil" - "os" - "path" - - "launchpad.net/goyaml" - - "launchpad.net/juju-core/state" - "launchpad.net/juju-core/state/api" - "launchpad.net/juju-core/utils" -) - -const format_1_12 = "format 1.12" - -// formatter_1_12 is the formatter for the 1.12 format. -type formatter_1_12 struct { -} - -// format_1_12Serialization holds information stored in the agent.conf file. -type format_1_12Serialization struct { - // StateServerCert and StateServerKey hold the state server - // certificate and private key in PEM format. - StateServerCert []byte `yaml:",omitempty"` - StateServerKey []byte `yaml:",omitempty"` - - StatePort int `yaml:",omitempty"` - APIPort int `yaml:",omitempty"` - - // OldPassword specifies a password that should be - // used to connect to the state if StateInfo.Password - // is blank or invalid. - OldPassword string - - // MachineNonce is set at provisioning/bootstrap time and used to - // ensure the agent is running on the correct instance. - MachineNonce string - - // StateInfo specifies how the agent should connect to the - // state. The password may be empty if an old password is - // specified, or when bootstrapping. - StateInfo *state.Info `yaml:",omitempty"` - - // OldAPIPassword specifies a password that should - // be used to connect to the API if APIInfo.Password - // is blank or invalid. - OldAPIPassword string - - // APIInfo specifies how the agent should connect to the - // state through the API. - APIInfo *api.Info `yaml:",omitempty"` -} - -// Ensure that the formatter_1_12 struct implements the formatter interface. -var _ formatter = (*formatter_1_12)(nil) - -func (*formatter_1_12) configFile(dirName string) string { - return path.Join(dirName, "agent.conf") -} - -func (formatter *formatter_1_12) read(dirName string) (*configInternal, error) { - data, err := ioutil.ReadFile(formatter.configFile(dirName)) - if err != nil { - return nil, err - } - var conf format_1_12Serialization - if err := goyaml.Unmarshal(data, &conf); err != nil { - return nil, err - } - - var stateDetails *connectionDetails - var caCert []byte - var tag string - if conf.StateInfo != nil { - stateDetails = &connectionDetails{ - conf.StateInfo.Addrs, - conf.StateInfo.Password, - } - tag = conf.StateInfo.Tag - caCert = conf.StateInfo.CACert - } - var apiDetails *connectionDetails - if conf.APIInfo != nil { - apiDetails = &connectionDetails{ - conf.APIInfo.Addrs, - conf.APIInfo.Password, - } - tag = conf.APIInfo.Tag - caCert = conf.APIInfo.CACert - } - return &configInternal{ - tag: tag, - nonce: conf.MachineNonce, - caCert: caCert, - stateDetails: stateDetails, - apiDetails: apiDetails, - oldPassword: conf.OldPassword, - stateServerCert: conf.StateServerCert, - stateServerKey: conf.StateServerKey, - apiPort: conf.APIPort, - values: map[string]string{}, - }, nil -} - -func (formatter *formatter_1_12) makeAgentConf(config *configInternal) *format_1_12Serialization { - format := &format_1_12Serialization{ - StateServerCert: config.stateServerCert, - StateServerKey: config.stateServerKey, - APIPort: config.apiPort, - OldPassword: config.oldPassword, - MachineNonce: config.nonce, - } - if config.stateDetails != nil { - // It is fine that we are copying the slices for the addresses. - format.StateInfo = &state.Info{ - Addrs: config.stateDetails.addresses, - Password: config.stateDetails.password, - Tag: config.tag, - CACert: config.caCert, - } - } - if config.apiDetails != nil { - format.APIInfo = &api.Info{ - Addrs: config.apiDetails.addresses, - Password: config.apiDetails.password, - Tag: config.tag, - CACert: config.caCert, - } - } - return format -} - -func (formatter *formatter_1_12) write(config *configInternal) error { - dirName := config.Dir() - conf := formatter.makeAgentConf(config) - data, err := goyaml.Marshal(conf) - if err != nil { - return err - } - if err := os.MkdirAll(dirName, 0755); err != nil { - return err - } - newFile := path.Join(dirName, "agent.conf-new") - if err := ioutil.WriteFile(newFile, data, 0600); err != nil { - return err - } - if err := os.Rename(newFile, formatter.configFile(dirName)); err != nil { - return err - } - return nil -} - -func (formatter *formatter_1_12) writeCommands(config *configInternal) ([]string, error) { - dirName := config.Dir() - conf := formatter.makeAgentConf(config) - data, err := goyaml.Marshal(conf) - if err != nil { - return nil, err - } - var commands []string - addCommand := func(f string, a ...interface{}) { - commands = append(commands, fmt.Sprintf(f, a...)) - } - filename := utils.ShQuote(formatter.configFile(dirName)) - addCommand("mkdir -p %s", utils.ShQuote(dirName)) - addCommand("install -m %o /dev/null %s", 0600, filename) - addCommand(`printf '%%s\n' %s > %s`, utils.ShQuote(string(data)), filename) - return commands, nil -} - -func (*formatter_1_12) migrate(config *configInternal) { -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.12_whitebox_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.12_whitebox_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.12_whitebox_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.12_whitebox_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package agent - -import ( - "os" - "path" - - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" - "launchpad.net/juju-core/testing/testbase" - "launchpad.net/juju-core/version" -) - -type format_1_12Suite struct { - testbase.LoggingSuite - formatter formatter_1_12 -} - -var _ = gc.Suite(&format_1_12Suite{}) - -func newTestConfig(c *gc.C) *configInternal { - params := agentParams - params.DataDir = c.MkDir() - config, err := NewAgentConfig(params) - c.Assert(err, gc.IsNil) - return config.(*configInternal) -} - -func (s *format_1_12Suite) TestWriteAgentConfig(c *gc.C) { - config := newTestConfig(c) - err := s.formatter.write(config) - c.Assert(err, gc.IsNil) - - expectedLocation := path.Join(config.Dir(), "agent.conf") - fileInfo, err := os.Stat(expectedLocation) - c.Assert(err, gc.IsNil) - c.Assert(fileInfo.Mode().IsRegular(), jc.IsTrue) - c.Assert(fileInfo.Mode().Perm(), gc.Equals, os.FileMode(0600)) - c.Assert(fileInfo.Size(), jc.GreaterThan, 0) -} - -func (s *format_1_12Suite) assertWriteAndRead(c *gc.C, config *configInternal) { - // Format 1.12 doesn't know about upgradedToVersion so zero it out. - config.upgradedToVersion = version.Zero - err := s.formatter.write(config) - c.Assert(err, gc.IsNil) - // The readConfig is missing the dataDir initially. - readConfig, err := s.formatter.read(config.Dir()) - c.Assert(err, gc.IsNil) - c.Assert(readConfig.dataDir, gc.Equals, "") - // This is put in by the ReadConf method that we are avoiding using - // becuase it will have side-effects soon around migrating configs. - readConfig.dataDir = config.dataDir - c.Assert(readConfig, gc.DeepEquals, config) -} - -func (s *format_1_12Suite) TestRead(c *gc.C) { - config := newTestConfig(c) - s.assertWriteAndRead(c, config) -} - -func (s *format_1_12Suite) TestWriteCommands(c *gc.C) { - config := newTestConfig(c) - commands, err := s.formatter.writeCommands(config) - c.Assert(err, gc.IsNil) - c.Assert(commands, gc.HasLen, 3) - c.Assert(commands[0], gc.Matches, `mkdir -p '\S+/agents/omg'`) - c.Assert(commands[1], gc.Matches, `install -m 600 /dev/null '\S+/agents/omg/agent.conf'`) - c.Assert(commands[2], gc.Matches, `printf '%s\\n' '(.|\n)*' > '\S+/agents/omg/agent.conf'`) -} - -func (s *format_1_12Suite) TestReadWriteStateConfig(c *gc.C) { - stateParams := StateMachineConfigParams{ - AgentConfigParams: agentParams, - StateServerCert: []byte("some special cert"), - StateServerKey: []byte("a special key"), - StatePort: 12345, - APIPort: 23456, - } - stateParams.DataDir = c.MkDir() - configInterface, err := NewStateMachineConfig(stateParams) - c.Assert(err, gc.IsNil) - config, ok := configInterface.(*configInternal) - c.Assert(ok, jc.IsTrue) - - s.assertWriteAndRead(c, config) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.16.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.16.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.16.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.16.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,35 +5,26 @@ import ( "encoding/base64" - "io/ioutil" - "os" - "path" "launchpad.net/goyaml" - - "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/version" ) -const ( - format_1_16 = "format 1.16" - // Old environment variables that are now stored in agent config. - jujuLxcBridge = "JUJU_LXC_BRIDGE" - jujuProviderType = "JUJU_PROVIDER_TYPE" - jujuStorageDir = "JUJU_STORAGE_DIR" - jujuStorageAddr = "JUJU_STORAGE_ADDR" -) +var format_1_16 = formatter_1_16{} // formatter_1_16 is the formatter for the 1.16 format. type formatter_1_16 struct { } +// Ensure that the formatter_1_16 struct implements the formatter interface. +var _ formatter = formatter_1_16{} + // format_1_16Serialization holds information for a given agent. type format_1_16Serialization struct { Tag string Nonce string - UpgradedToVersion string `yaml:"upgradedToVersion"` - // CACert is base64 encoded + UpgradedToVersion *version.Number `yaml:"upgradedToVersion"` + CACert string StateAddresses []string `yaml:",omitempty"` StatePassword string `yaml:",omitempty"` @@ -50,60 +41,56 @@ APIPort int `yaml:",omitempty"` } -// Ensure that the formatter_1_16 struct implements the formatter interface. -var _ formatter = (*formatter_1_16)(nil) - -func (*formatter_1_16) configFile(dirName string) string { - return path.Join(dirName, "agent.conf") +func init() { + registerFormat(format_1_16) } +const legacyFormatFilename = "format" + +// legacyFormatPrefix is the prefix of the legacy format file. +const legacyFormatPrefix = "format " + // decode64 makes sure that for an empty string we have a nil slice, not an // empty slice, which is what the base64 DecodeString function returns. -func (*formatter_1_16) decode64(value string) (result []byte, err error) { +func decode64(value string) (result []byte, err error) { if value != "" { result, err = base64.StdEncoding.DecodeString(value) } return } -// upgradedToVersion parses the upgradedToVersion string value into a version.Number. -// An empty value is returned as 1.16.0. -func (*formatter_1_16) upgradedToVersion(value string) (version.Number, error) { - if value != "" { - return version.Parse(value) - } - return version.MustParse("1.16.0"), nil +func (formatter_1_16) version() string { + return "1.16" } -func (formatter *formatter_1_16) read(dirName string) (*configInternal, error) { - data, err := ioutil.ReadFile(formatter.configFile(dirName)) - if err != nil { - return nil, err - } +func (formatter_1_16) unmarshal(data []byte) (*configInternal, error) { var format format_1_16Serialization if err := goyaml.Unmarshal(data, &format); err != nil { return nil, err } - caCert, err := formatter.decode64(format.CACert) + caCert, err := decode64(format.CACert) if err != nil { return nil, err } - stateServerCert, err := formatter.decode64(format.StateServerCert) + stateServerCert, err := decode64(format.StateServerCert) if err != nil { return nil, err } - stateServerKey, err := formatter.decode64(format.StateServerKey) + stateServerKey, err := decode64(format.StateServerKey) if err != nil { return nil, err } - upgradedToVersion, err := formatter.upgradedToVersion(format.UpgradedToVersion) - if err != nil { - return nil, err + if format.UpgradedToVersion == nil { + // Assume it's 1.16.0. + upgradedToVersion := version.MustParse("1.16.0") + format.UpgradedToVersion = &upgradedToVersion } config := &configInternal{ tag: format.Tag, nonce: format.Nonce, - upgradedToVersion: upgradedToVersion, + dataDir: DefaultDataDir, + logDir: DefaultLogDir, + upgradedToVersion: *format.UpgradedToVersion, caCert: caCert, oldPassword: format.OldPassword, stateServerCert: stateServerCert, @@ -125,92 +112,3 @@ } return config, nil } - -func (formatter *formatter_1_16) makeFormat(config *configInternal) *format_1_16Serialization { - format := &format_1_16Serialization{ - Tag: config.tag, - Nonce: config.nonce, - UpgradedToVersion: config.upgradedToVersion.String(), - CACert: base64.StdEncoding.EncodeToString(config.caCert), - OldPassword: config.oldPassword, - StateServerCert: base64.StdEncoding.EncodeToString(config.stateServerCert), - StateServerKey: base64.StdEncoding.EncodeToString(config.stateServerKey), - APIPort: config.apiPort, - Values: config.values, - } - if config.stateDetails != nil { - format.StateAddresses = config.stateDetails.addresses - format.StatePassword = config.stateDetails.password - } - if config.apiDetails != nil { - format.APIAddresses = config.apiDetails.addresses - format.APIPassword = config.apiDetails.password - } - return format -} - -func (formatter *formatter_1_16) write(config *configInternal) error { - dirName := config.Dir() - conf := formatter.makeFormat(config) - data, err := goyaml.Marshal(conf) - if err != nil { - return err - } - // TODO(thumper): 2013-09-13 bug 1224725 - // We should be writing to a temp dir first then rename. - // Writing the format file makes sure that dirName exists. We should - // really be writing the format and new config files into a separate - // directory, and renaming the directory, and moving the old agend - // directory to ".old". - if err := writeFormatFile(dirName, format_1_16); err != nil { - return err - } - newFile := path.Join(dirName, "agent.conf-new") - if err := ioutil.WriteFile(newFile, data, 0600); err != nil { - return err - } - if err := os.Rename(newFile, formatter.configFile(dirName)); err != nil { - return err - } - return nil -} - -func (formatter *formatter_1_16) writeCommands(config *configInternal) ([]string, error) { - dirName := config.Dir() - conf := formatter.makeFormat(config) - data, err := goyaml.Marshal(conf) - if err != nil { - return nil, err - } - commands := writeCommandsForFormat(dirName, format_1_16) - commands = append(commands, - writeFileCommands(formatter.configFile(dirName), string(data), 0600)...) - return commands, nil -} - -func (*formatter_1_16) migrate(config *configInternal) { - for _, name := range []struct { - environment string - config string - }{{ - jujuProviderType, - ProviderType, - }, { - osenv.JujuContainerTypeEnvKey, - ContainerType, - }, { - jujuLxcBridge, - LxcBridge, - }, { - jujuStorageDir, - StorageDir, - }, { - jujuStorageAddr, - StorageAddr, - }} { - value := os.Getenv(name.environment) - if value != "" { - config.values[name.config] = value - } - } -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.16_whitebox_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.16_whitebox_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.16_whitebox_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.16_whitebox_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,49 +9,89 @@ import ( "io/ioutil" - "os" - "path" "path/filepath" - gc "launchpad.net/gocheck" + jc "github.com/juju/testing/checkers" - "launchpad.net/juju-core/juju/osenv" - jc "launchpad.net/juju-core/testing/checkers" + gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) type format_1_16Suite struct { testbase.LoggingSuite - formatter formatter_1_16 } var _ = gc.Suite(&format_1_16Suite{}) -func (s *format_1_16Suite) TestWriteAgentConfig(c *gc.C) { - config := newTestConfig(c) - err := s.formatter.write(config) +func (s *format_1_16Suite) TestMissingAttributes(c *gc.C) { + dataDir := c.MkDir() + formatPath := filepath.Join(dataDir, legacyFormatFilename) + err := utils.AtomicWriteFile(formatPath, []byte(legacyFormatFileContents), 0600) c.Assert(err, gc.IsNil) + configPath := filepath.Join(dataDir, agentConfigFilename) + err = utils.AtomicWriteFile(configPath, []byte(configDataWithoutNewAttributes), 0600) + c.Assert(err, gc.IsNil) + readConfig, err := ReadConf(configPath) + c.Assert(err, gc.IsNil) + c.Assert(readConfig.UpgradedToVersion(), gc.Equals, version.MustParse("1.16.0")) + c.Assert(readConfig.LogDir(), gc.Equals, "/var/log/juju") + c.Assert(readConfig.DataDir(), gc.Equals, "/var/lib/juju") +} - expectedLocation := path.Join(config.Dir(), "agent.conf") - fileInfo, err := os.Stat(expectedLocation) +func (*format_1_16Suite) TestReadConfReadsLegacyFormatAndWritesNew(c *gc.C) { + dataDir := c.MkDir() + formatPath := filepath.Join(dataDir, legacyFormatFilename) + err := utils.AtomicWriteFile(formatPath, []byte(legacyFormatFileContents), 0600) + c.Assert(err, gc.IsNil) + configPath := filepath.Join(dataDir, agentConfigFilename) + err = utils.AtomicWriteFile(configPath, []byte(agentConfig1_16Contents), 0600) c.Assert(err, gc.IsNil) - c.Assert(fileInfo.Mode().IsRegular(), jc.IsTrue) - c.Assert(fileInfo.Mode().Perm(), gc.Equals, os.FileMode(0600)) - c.Assert(fileInfo.Size(), jc.GreaterThan, 0) - formatLocation := path.Join(config.Dir(), formatFilename) - fileInfo, err = os.Stat(formatLocation) + config, err := ReadConf(configPath) c.Assert(err, gc.IsNil) - c.Assert(fileInfo.Mode().IsRegular(), jc.IsTrue) - c.Assert(fileInfo.Mode().Perm(), gc.Equals, os.FileMode(0644)) - c.Assert(fileInfo.Size(), jc.GreaterThan, 0) + c.Assert(config, gc.NotNil) + // Test we wrote a currently valid config. + config, err = ReadConf(configPath) + c.Assert(err, gc.IsNil) + c.Assert(config, gc.NotNil) + c.Assert(config.UpgradedToVersion(), jc.DeepEquals, version.MustParse("1.16.0")) + c.Assert(config.Jobs(), gc.HasLen, 0) - formatContent, err := readFormat(config.Dir()) - c.Assert(formatContent, gc.Equals, format_1_16) + // Old format was deleted. + assertFileNotExist(c, formatPath) + // And new contents were written. + data, err := ioutil.ReadFile(configPath) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Not(gc.Equals), agentConfig1_16Contents) } -var configDataWithoutUpgradedToVersion = ` +const legacyFormatFileContents = "format 1.16" + +var agentConfig1_16Contents = ` +tag: machine-0 +nonce: user-admin:bootstrap +cacert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNXekNDQWNhZ0F3SUJBZ0lCQURBTEJna3Foa2lHOXcwQkFRVXdRekVOTUFzR0ExVUVDaE1FYW5WcWRURXkKTURBR0ExVUVBd3dwYW5WcWRTMW5aVzVsY21GMFpXUWdRMEVnWm05eUlHVnVkbWx5YjI1dFpXNTBJQ0pzYjJOaApiQ0l3SGhjTk1UUXdNekExTVRJeE9ERTJXaGNOTWpRd016QTFNVEl5TXpFMldqQkRNUTB3Q3dZRFZRUUtFd1JxCmRXcDFNVEl3TUFZRFZRUUREQ2xxZFdwMUxXZGxibVZ5WVhSbFpDQkRRU0JtYjNJZ1pXNTJhWEp2Ym0xbGJuUWcKSW14dlkyRnNJakNCbnpBTkJna3Foa2lHOXcwQkFRRUZBQU9CalFBd2dZa0NnWUVBd3NaVUg3NUZGSW1QUWVGSgpaVnVYcmlUWmNYdlNQMnk0VDJaSU5WNlVrY2E5VFdXb01XaWlPYm4yNk03MjNGQllPczh3WHRYNEUxZ2l1amxYCmZGeHNFckloczEyVXQ1S3JOVkkyMlEydCtVOGViakZMUHJiUE5Fb3pzdnU3UzFjZklFbjBXTVg4MWRBaENOMnQKVkxGaC9hS3NqSHdDLzJ5Y3Z0VSttTngyVG5FQ0F3RUFBYU5qTUdFd0RnWURWUjBQQVFIL0JBUURBZ0NrTUE4RwpBMVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZKVUxKZVlIbERsdlJ3T0owcWdyemcwclZGZUVNQjhHCkExVWRJd1FZTUJhQUZKVUxKZVlIbERsdlJ3T0owcWdyemcwclZGZUVNQXNHQ1NxR1NJYjNEUUVCQlFPQmdRQ2UKRlRZbThsWkVYZUp1cEdPc3pwc2pjaHNSMEFxeXROZ1dxQmE1cWUyMS9xS2R3TUFSQkNFMTU3eUxGVnl6MVoycQp2YVhVNy9VKzdKbGNiWmtHRHJ5djE2S2UwK2RIY3NEdG5jR2FOVkZKMTAxYnNJNG1sVEkzQWpQNDErNG5mQ0VlCmhwalRvYm1YdlBhOFN1NGhQYTBFc1E4bXFaZGFabmdwRU0vb1JiZ0RMdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K +stateaddresses: +- localhost:37017 +statepassword: OlUMkte5J3Ss0CH9yxedilIC +apiaddresses: +- localhost:17070 +apipassword: OlUMkte5J3Ss0CH9yxedilIC +oldpassword: oBlMbFUGvCb2PMFgYVzjS6GD +values: + PROVIDER_TYPE: local + SHARED_STORAGE_ADDR: 10.0.3.1:8041 + SHARED_STORAGE_DIR: /home/user/.juju/local/shared-storage + STORAGE_ADDR: 10.0.3.1:8040 + STORAGE_DIR: /home/user/.juju/local/storage +stateservercert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNJakNDQVkyZ0F3SUJBZ0lCQURBTEJna3Foa2lHOXcwQkFRVXdRekVOTUFzR0ExVUVDaE1FYW5WcWRURXkKTURBR0ExVUVBd3dwYW5WcWRTMW5aVzVsY21GMFpXUWdRMEVnWm05eUlHVnVkbWx5YjI1dFpXNTBJQ0pzYjJOaApiQ0l3SGhjTk1UUXdNekExTVRJeE9ESXlXaGNOTWpRd016QTFNVEl5TXpJeVdqQWJNUTB3Q3dZRFZRUUtFd1JxCmRXcDFNUW93Q0FZRFZRUURFd0VxTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDdVA0dTAKQjZtbGs0V0g3SHFvOXhkSFp4TWtCUVRqV2VLTkhERzFMb21SWmc2RHA4Z0VQK0ZNVm5IaUprZW1pQnJNSEk3OAo5bG4zSVRBT0NJT0xna0NkN3ZsaDJub2FheTlSeXpUaG9PZ0RMSzVpR0VidmZDeEFWZThhWDQvbThhOGNLWE9TCmJJZTZFNnVtb0wza0JNaEdiL1QrYW1xbHRjaHVNRXJhanJSVit3SURBUUFCbzFJd1VEQU9CZ05WSFE4QkFmOEUKQkFNQ0FCQXdIUVlEVlIwT0JCWUVGRTV1RFg3UlRjckF2ajFNcWpiU2w1M21pR0NITUI4R0ExVWRJd1FZTUJhQQpGSlVMSmVZSGxEbHZSd09KMHFncnpnMHJWRmVFTUFzR0NTcUdTSWIzRFFFQkJRT0JnUUJUNC8vZkpESUcxM2dxClBiamNnUTN6eHh6TG12STY5Ty8zMFFDbmIrUGZObDRET0U1SktwVE5OTjhkOEJEQWZPYStvWE5neEM3VTZXdjUKZjBYNzEyRnlNdUc3VXJEVkNDY0kxS3JSQ0F0THlPWUREL0ZPblBwSWdVQjF1bFRnOGlRUzdlTjM2d0NEL21wVApsUVVUS2FuU00yMnhnWWJKazlRY1dBSzQ0ZjA4SEE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== +stateserverkey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDV3dJQkFBS0JnUUN1UDR1MEI2bWxrNFdIN0hxbzl4ZEhaeE1rQlFUaldlS05IREcxTG9tUlpnNkRwOGdFClArRk1WbkhpSmtlbWlCck1ISTc4OWxuM0lUQU9DSU9MZ2tDZDd2bGgybm9hYXk5Unl6VGhvT2dETEs1aUdFYnYKZkN4QVZlOGFYNC9tOGE4Y0tYT1NiSWU2RTZ1bW9MM2tCTWhHYi9UK2FtcWx0Y2h1TUVyYWpyUlYrd0lEQVFBQgpBb0dBRERJZ2FoSmJPbDZQNndxUEwwSlVHOGhJRzY1S1FFdHJRdXNsUTRRbFZzcm8yeWdrSkwvLzJlTDNCNWdjClRiaWEvNHhFS2Nwb1U1YThFVTloUGFONU9EYnlkVEsxQ1I3R2JXSGkwWm1LbGZCUlR4bUpxakdKVU1CSmI4a0QKNStpMzlvcXdQS3dnaXoyTVR5SHZKZFFJVHB0ZDVrbEQyYjU1by9YWFRCTnk2NGtDUVFEbXRFWHNTL2kxTm5pSwozZVJkeHM4UVFGN0pKVG5SR042ZUh6ZHlXb242Zjl2ZkxrSDROWUdxcFUydjVBNUl1Nno3K3NJdXVHU2ZSeEI1CktrZVFXdlVQQWtFQXdWcVdlczdmc3NLbUFCZGxER3ozYzNxMjI2eVVaUE00R3lTb1cxYXZsYzJ1VDVYRm9vVUsKNjRpUjJuU2I1OHZ2bGY1RjRRMnJuRjh2cFRLcFJwK0lWUUpBTlcwZ0dFWEx0ZU9FYk54UUMydUQva1o1N09rRApCNnBUdTVpTkZaMWtBSy9sY2p6YktDanorMW5Hc09vR2FNK1ZrdEVTY1JGZ3RBWVlDWWRDQldzYS93SkFROWJXCnlVdmdMTVlpbkJHWlFKelN6VStXN01oR1lJejllSGlLSVZIdTFTNlBKQmsyZUdrWmhiNHEvbXkvYnJxYzJ4R1YKenZxTzVaUjRFUXdQWEZvSTZRSkFkeVdDMllOTTF2a1BuWnJqZzNQQXFHRHJQMHJwNEZ0bFV4alh0ay8vcW9hNgpRcXVYcE9vNjd4THRieW1PTlJTdDFiZGE5ZE5tbGljMFVNZ0JQRHgrYnc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= +apiport: 17070 +`[1:] + +const configDataWithoutNewAttributes = ` tag: omg nonce: a nonce cacert: Y2EgY2VydA== @@ -62,94 +102,3 @@ oldpassword: sekrit values: {} ` - -func (s *format_1_16Suite) TestMissingUpgradedToVersion(c *gc.C) { - dataDir := c.MkDir() - err := ioutil.WriteFile(filepath.Join(dataDir, "agent.conf"), []byte(configDataWithoutUpgradedToVersion), 0600) - c.Assert(err, gc.IsNil) - readConfig, err := s.formatter.read(dataDir) - c.Assert(err, gc.IsNil) - c.Assert(readConfig.UpgradedToVersion(), gc.Equals, version.MustParse("1.16.0")) -} - -func (s *format_1_16Suite) assertWriteAndRead(c *gc.C, config *configInternal) { - err := s.formatter.write(config) - c.Assert(err, gc.IsNil) - // The readConfig is missing the dataDir initially. - readConfig, err := s.formatter.read(config.Dir()) - c.Assert(err, gc.IsNil) - c.Assert(readConfig.dataDir, gc.Equals, "") - // This is put in by the ReadConf method that we are avoiding using - // because it will have side-effects soon around migrating configs. - readConfig.dataDir = config.dataDir - c.Assert(readConfig, gc.DeepEquals, config) -} - -func (s *format_1_16Suite) TestRead(c *gc.C) { - config := newTestConfig(c) - s.assertWriteAndRead(c, config) -} - -func (s *format_1_16Suite) TestWriteCommands(c *gc.C) { - config := newTestConfig(c) - commands, err := s.formatter.writeCommands(config) - c.Assert(err, gc.IsNil) - c.Assert(commands, gc.HasLen, 5) - c.Assert(commands[0], gc.Matches, `mkdir -p '\S+/agents/omg'`) - c.Assert(commands[1], gc.Matches, `install -m 644 /dev/null '\S+/agents/omg/format'`) - c.Assert(commands[2], gc.Matches, `printf '%s\\n' '.*' > '\S+/agents/omg/format'`) - c.Assert(commands[3], gc.Matches, `install -m 600 /dev/null '\S+/agents/omg/agent.conf'`) - c.Assert(commands[4], gc.Matches, `printf '%s\\n' '(.|\n)*' > '\S+/agents/omg/agent.conf'`) -} - -func (s *format_1_16Suite) TestReadWriteStateConfig(c *gc.C) { - stateParams := StateMachineConfigParams{ - AgentConfigParams: agentParams, - StateServerCert: []byte("some special cert"), - StateServerKey: []byte("a special key"), - StatePort: 12345, - APIPort: 23456, - } - stateParams.DataDir = c.MkDir() - stateParams.Values = map[string]string{"foo": "bar", "wibble": "wobble"} - configInterface, err := NewStateMachineConfig(stateParams) - c.Assert(err, gc.IsNil) - config, ok := configInterface.(*configInternal) - c.Assert(ok, jc.IsTrue) - - s.assertWriteAndRead(c, config) -} - -func (s *format_1_16Suite) TestMigrate(c *gc.C) { - s.PatchEnvironment(jujuLxcBridge, "lxc bridge") - s.PatchEnvironment(jujuProviderType, "provider type") - s.PatchEnvironment(osenv.JujuContainerTypeEnvKey, "container type") - s.PatchEnvironment(jujuStorageDir, "storage dir") - s.PatchEnvironment(jujuStorageAddr, "storage addr") - - config := newTestConfig(c) - s.formatter.migrate(config) - - expected := map[string]string{ - LxcBridge: "lxc bridge", - ProviderType: "provider type", - ContainerType: "container type", - StorageDir: "storage dir", - StorageAddr: "storage addr", - } - - c.Assert(config.values, gc.DeepEquals, expected) -} - -func (s *format_1_16Suite) TestMigrateOnlySetsExisting(c *gc.C) { - s.PatchEnvironment(jujuProviderType, "provider type") - - config := newTestConfig(c) - s.formatter.migrate(config) - - expected := map[string]string{ - ProviderType: "provider type", - } - - c.Assert(config.values, gc.DeepEquals, expected) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.18.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.18.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.18.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.18.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,123 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package agent + +import ( + "launchpad.net/goyaml" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/version" +) + +var format_1_18 = formatter_1_18{} + +// formatter_1_18 is the formatter for the 1.18 format. +type formatter_1_18 struct { +} + +// Ensure that the formatter_1_18 struct implements the formatter interface. +var _ formatter = formatter_1_18{} + +// format_1_18Serialization holds information for a given agent. +type format_1_18Serialization struct { + Tag string + DataDir string + LogDir string + Nonce string + Jobs []params.MachineJob `yaml:",omitempty"` + UpgradedToVersion *version.Number `yaml:"upgradedToVersion"` + + CACert string + StateAddresses []string `yaml:",omitempty"` + StatePassword string `yaml:",omitempty"` + + APIAddresses []string `yaml:",omitempty"` + APIPassword string `yaml:",omitempty"` + + OldPassword string + Values map[string]string + + // Only state server machines have these next three items + StateServerCert string `yaml:",omitempty"` + StateServerKey string `yaml:",omitempty"` + APIPort int `yaml:",omitempty"` +} + +func init() { + registerFormat(format_1_18) +} + +func (formatter_1_18) version() string { + return "1.18" +} + +func (formatter_1_18) unmarshal(data []byte) (*configInternal, error) { + var format format_1_18Serialization + if err := goyaml.Unmarshal(data, &format); err != nil { + return nil, err + } + if format.UpgradedToVersion == nil || *format.UpgradedToVersion == version.Zero { + // Assume we upgrade from 1.16. + upgradedToVersion := version.MustParse("1.16.0") + format.UpgradedToVersion = &upgradedToVersion + } + config := &configInternal{ + tag: format.Tag, + dataDir: format.DataDir, + logDir: format.LogDir, + jobs: format.Jobs, + upgradedToVersion: *format.UpgradedToVersion, + nonce: format.Nonce, + caCert: []byte(format.CACert), + oldPassword: format.OldPassword, + stateServerCert: []byte(format.StateServerCert), + stateServerKey: []byte(format.StateServerKey), + apiPort: format.APIPort, + values: format.Values, + } + if config.logDir == "" { + config.logDir = DefaultLogDir + } + if config.dataDir == "" { + config.dataDir = DefaultDataDir + } + if len(format.StateAddresses) > 0 { + config.stateDetails = &connectionDetails{ + format.StateAddresses, + format.StatePassword, + } + } + if len(format.APIAddresses) > 0 { + config.apiDetails = &connectionDetails{ + format.APIAddresses, + format.APIPassword, + } + } + return config, nil +} + +func (formatter_1_18) marshal(config *configInternal) ([]byte, error) { + format := &format_1_18Serialization{ + Tag: config.tag, + DataDir: config.dataDir, + LogDir: config.logDir, + Jobs: config.jobs, + UpgradedToVersion: &config.upgradedToVersion, + Nonce: config.nonce, + CACert: string(config.caCert), + OldPassword: config.oldPassword, + StateServerCert: string(config.stateServerCert), + StateServerKey: string(config.stateServerKey), + APIPort: config.apiPort, + Values: config.values, + } + if config.stateDetails != nil { + format.StateAddresses = config.stateDetails.addresses + format.StatePassword = config.stateDetails.password + } + if config.apiDetails != nil { + format.APIAddresses = config.apiDetails.addresses + format.APIPassword = config.apiDetails.password + } + return goyaml.Marshal(format) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.18_whitebox_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.18_whitebox_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format-1.18_whitebox_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format-1.18_whitebox_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,170 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +// The format tests are white box tests, meaning that the tests are in the +// same package as the code, as all the format details are internal to the +// package. + +package agent + +import ( + "path/filepath" + + jc "github.com/juju/testing/checkers" + + gc "launchpad.net/gocheck" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/version" +) + +type format_1_18Suite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&format_1_18Suite{}) + +var configData1_18WithoutUpgradedToVersion = "# format 1.18\n" + configDataWithoutNewAttributes + +func (s *format_1_18Suite) TestMissingAttributes(c *gc.C) { + dataDir := c.MkDir() + configPath := filepath.Join(dataDir, agentConfigFilename) + err := utils.AtomicWriteFile(configPath, []byte(configData1_18WithoutUpgradedToVersion), 0600) + c.Assert(err, gc.IsNil) + readConfig, err := ReadConf(configPath) + c.Assert(err, gc.IsNil) + c.Assert(readConfig.UpgradedToVersion(), gc.Equals, version.MustParse("1.16.0")) + c.Assert(readConfig.LogDir(), gc.Equals, "/var/log/juju") + c.Assert(readConfig.DataDir(), gc.Equals, "/var/lib/juju") +} + +func (*format_1_18Suite) TestReadConfWithExisting1_18ConfigFileContents(c *gc.C) { + dataDir := c.MkDir() + configPath := filepath.Join(dataDir, agentConfigFilename) + err := utils.AtomicWriteFile(configPath, []byte(agentConfig1_18Contents), 0600) + c.Assert(err, gc.IsNil) + + config, err := ReadConf(configPath) + c.Assert(err, gc.IsNil) + c.Assert(config.UpgradedToVersion(), jc.DeepEquals, version.MustParse("1.17.5.1")) + c.Assert(config.Jobs(), jc.DeepEquals, []params.MachineJob{params.JobManageEnviron}) +} + +var agentConfig1_18Contents = ` +# format 1.18 +tag: machine-0 +datadir: /home/user/.juju/local +logdir: /var/log/juju-user-local +nonce: user-admin:bootstrap +jobs: +- JobManageEnviron +upgradedToVersion: 1.17.5.1 +cacert: '-----BEGIN CERTIFICATE----- + + MIICWzCCAcagAwIBAgIBADALBgkqhkiG9w0BAQUwQzENMAsGA1UEChMEanVqdTEy + + MDAGA1UEAwwpanVqdS1nZW5lcmF0ZWQgQ0EgZm9yIGVudmlyb25tZW50ICJsb2Nh + + bCIwHhcNMTQwMzA1MTQxOTA3WhcNMjQwMzA1MTQyNDA3WjBDMQ0wCwYDVQQKEwRq + + dWp1MTIwMAYDVQQDDClqdWp1LWdlbmVyYXRlZCBDQSBmb3IgZW52aXJvbm1lbnQg + + ImxvY2FsIjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwHsKV7fKfmSQt2QL + + P4+hrqQJhDTMifgNkIY9nTlLHegV5jl5XJ8lRYjZBXJEMz0AzW/RbrDElkn5+4Do + + pIWPNDAT0eztXBvVwL6qQOUtiBsA7vHQJMQaLVAmZNKvrHyuhcoG+hpf8EMaLdbA + + iCGKifs+Y0MFt5AeriVDH5lGlzcCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgCkMA8G + + A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFB3Td3SP66UToZkOjVh3Wy8b6HR6MB8G + + A1UdIwQYMBaAFB3Td3SP66UToZkOjVh3Wy8b6HR6MAsGCSqGSIb3DQEBBQOBgQB4 + + izvSRSpimi40aEOnZIsSMHVBiSCclpBg5cq7lGyiUSsDROTIbsRAKPBmrflB/qbf + + J70rWFwh/d/5ssCAYrZviFL6WvpuLD3j3m4PYampNMmvJf2s6zVRIMotEY+bVwfU + + z4jGaVpODac0i0bE0/Uh9qXK1UXcYY57vNNAgkaYAQ== + + -----END CERTIFICATE----- + +' +stateaddresses: +- localhost:37017 +statepassword: NB5imrDaWCCRW/4akSSvUxhX +apiaddresses: +- localhost:17070 +apipassword: NB5imrDaWCCRW/4akSSvUxhX +oldpassword: oBlMbFUGvCb2PMFgYVzjS6GD +values: + AGENT_SERVICE_NAME: juju-agent-user-local + CONTAINER_TYPE: "" + MONGO_SERVICE_NAME: juju-db-user-local + NAMESPACE: user-local + PROVIDER_TYPE: local + STORAGE_ADDR: 10.0.3.1:8040 + STORAGE_DIR: /home/user/.juju/local/storage +stateservercert: '-----BEGIN CERTIFICATE----- + + MIICNzCCAaKgAwIBAgIBADALBgkqhkiG9w0BAQUwQzENMAsGA1UEChMEanVqdTEy + + MDAGA1UEAwwpanVqdS1nZW5lcmF0ZWQgQ0EgZm9yIGVudmlyb25tZW50ICJsb2Nh + + bCIwHhcNMTQwMzA1MTQxOTE1WhcNMjQwMzA1MTQyNDE1WjAbMQ0wCwYDVQQKEwRq + + dWp1MQowCAYDVQQDEwEqMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJnbuN + + L3m/oY7Er2lEF6ye1SodepvpI0CLCdLwrYP52cRxbVzoD1jbXveclolg2xoUquga + + qxsAhvVzzGaoLux1BoBD+G0N637fnY4XSIC9IuSkPOAdReKJkOvTL4nTjpzgfeHR + + hRin6Xckvp96L4Prmki7sYQ8PG9Q7TBcOf4yowIDAQABo2cwZTAOBgNVHQ8BAf8E + + BAMCAKgwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFE1MB3d+5BW+n066 + + lWcVkhta1etlMB8GA1UdIwQYMBaAFB3Td3SP66UToZkOjVh3Wy8b6HR6MAsGCSqG + + SIb3DQEBBQOBgQBnsBvl3hfIQbHhAlqritDBCWGpaXywlHe4PvyVL3LZTLiAZ9a/ + + BOSBfovs81sjUe5l60j+1vgRQgvT2Pnw6WGWmYWhSyxW7upEUl1LuZxnw3AVGVFO + + r140iBNUtTfGUf3PmyBXHSotqgMime+rNSjl25qSoYwnuQXdFdCKJoutYg== + + -----END CERTIFICATE----- + +' +stateserverkey: '-----BEGIN RSA PRIVATE KEY----- + + MIICXAIBAAKBgQDJnbuNL3m/oY7Er2lEF6ye1SodepvpI0CLCdLwrYP52cRxbVzo + + D1jbXveclolg2xoUqugaqxsAhvVzzGaoLux1BoBD+G0N637fnY4XSIC9IuSkPOAd + + ReKJkOvTL4nTjpzgfeHRhRin6Xckvp96L4Prmki7sYQ8PG9Q7TBcOf4yowIDAQAB + + AoGASEtzETFQ6tI3q3dqu6vxjhLJw0BP381wO2sOZJcTl+fqdPHOOrgmGKN5DoE8 + + SarHM1oFWGq6h/nc0eUdenk4+CokpbKRgUU9hB1TKGYMbN3bUTKPOqTMHbnrhWdT + + P/fqa+nXhvg7igMT3Rk7l9DsSxoYB5xZmiLaXqynVE5MNoECQQDRsgDDUrUOeMH6 + + 1+GO+afb8beRzR8mnaBvja6XLlZB6SUcGet9bMgAiGH3arH6ARfNNsWrDAmvArah + + SKeqRB5TAkEA9iMEQDkcybCmxu4Y3YLeQuT9r3h26QhQjc+eRINS/3ZLN+lxKnXG + + N019ZUlsyL97lJBDzTMPsBqfXJ2pbqXwcQJBAJNLuPN63kl7E68zA3Ld9UYvBWY6 + + Mp56bJ7PZAs39kk4DuQtZNhmmBqfskkMPlZBfEmfJrxeqVKw0j56faPBU5cCQFYU + + mP/8+VxwM2OPEZMmmaS7gR1E/BEznzh5S9iaNQSy0kuTkMhQuCnPJ/OsYiczEH08 + + lvnEyc/E/8bcPM09q4ECQCFwMWzw2Jx9VOBGm60yiOKIdLgdDZOY/tP0jigNCMJF + + 47/BJx3FCgW3io81a4KOc445LxgiPUJyyCNlY1dW70o= + + -----END RSA PRIVATE KEY----- + +' +apiport: 17070 +`[1:] diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,97 +4,101 @@ package agent import ( + "bytes" "fmt" - "io/ioutil" - "os" - "path" "strings" "launchpad.net/juju-core/utils" ) -// The format file in the agent config directory is used to identify the -// method of serialization. This is used by individual format readers and -// writers to be able to translate from the file format to the in-memory -// structure. +// Current agent config format is defined as follows: +// # format \n (very first line; is 1.18 or later) +// +// All of this is saved in a single agent.conf file. // -// Juju only supports upgrading from single steps, so Juju only needs to know -// about the current format and the format of the previous stable release. -// For convenience, the format name includes the version number of the stable -// release that it will be released with. Once this release has happened, the -// format should be considered FIXED, and should no longer be modified. If -// changes are necessary to the format, a new format should be created. +// Historically the format file in the agent config directory was used +// to identify the method of serialization. This was used by +// individual legacy (pre 1.18) format readers and writers to be able +// to translate from the file format to the in-memory structure. From +// version 1.18, the format is part of the agent configuration file, +// so there is only a single source of truth. // -// We don't need to create new formats for each release, the version number is -// just a convenience for us to know which stable release introduced that -// format. - -const ( - formatFilename = "format" - currentFormat = format_1_16 - previousFormat = format_1_12 -) +// Juju only supports upgrading from single steps, so Juju only needs +// to know about the current format and the format of the previous +// stable release. For convenience, the format name includes the +// version number of the stable release that it will be released with. +// Once this release has happened, the format should be considered +// FIXED, and should no longer be modified. If changes are necessary +// to the format, a new format should be created. +// +// We don't need to create new formats for each release, the version +// number is just a convenience for us to know which stable release +// introduced that format. -var ( - currentFormatter = &formatter_1_16{} - previousFormatter = &formatter_1_12{} -) +var formats = make(map[string]formatter) // The formatter defines the two methods needed by the formatters for // translating to and from the internal, format agnostic, structure. type formatter interface { - read(dirName string) (*configInternal, error) - write(config *configInternal) error - writeCommands(config *configInternal) ([]string, error) - // Migrate is called when upgrading from the previous format to the current format. - migrate(config *configInternal) + version() string + unmarshal(data []byte) (*configInternal, error) } -func formatFile(dirName string) string { - return path.Join(dirName, formatFilename) +func registerFormat(format formatter) { + formats[format.version()] = format } -func readFormat(dirName string) (string, error) { - contents, err := ioutil.ReadFile(formatFile(dirName)) - // Once the previousFormat is defined to have a format file (1.16 or - // above), not finding a format file should be a real error. - if err != nil { - return previousFormat, nil - } - return strings.TrimSpace(string(contents)), nil -} +// Once a new format version is introduced: +// - Create a formatter for the new version (including a marshal() method); +// - Call registerFormat in the new format's init() function. +// - Change this to point to the new format; +// - Remove the marshal() method from the old format; -func newFormatter(format string) (formatter, error) { - switch format { - case currentFormat: - return currentFormatter, nil - case previousFormat: - return previousFormatter, nil - } - return nil, fmt.Errorf("unknown agent config format") -} +// currentFormat holds the current agent config version's formatter. +var currentFormat = format_1_18 -func writeFormatFile(dirName string, format string) error { - if err := os.MkdirAll(dirName, 0755); err != nil { - return err - } - newFile := path.Join(dirName, formatFilename+"-new") - if err := ioutil.WriteFile(newFile, []byte(format+"\n"), 0644); err != nil { - return err - } - return os.Rename(newFile, formatFile(dirName)) -} +// agentConfigFilename is the default file name of used for the agent +// config. +const agentConfigFilename = "agent.conf" -func writeFileCommands(filename, contents string, permission int) []string { +// formatPrefix is prefix of the first line in an agent config file. +const formatPrefix = "# format " + +func writeFileCommands(filename string, contents []byte, permission int) []string { quotedFilename := utils.ShQuote(filename) + quotedContents := utils.ShQuote(string(contents)) return []string{ fmt.Sprintf("install -m %o /dev/null %s", permission, quotedFilename), - fmt.Sprintf(`printf '%%s\n' %s > %s`, utils.ShQuote(contents), quotedFilename), + fmt.Sprintf(`printf '%%s\n' %s > %s`, quotedContents, quotedFilename), } } -func writeCommandsForFormat(dirName, format string) []string { - commands := []string{"mkdir -p " + utils.ShQuote(dirName)} - commands = append(commands, writeFileCommands(formatFile(dirName), format, 0644)...) - return commands +func getFormatter(version string) (formatter, error) { + version = strings.TrimSpace(version) + format, ok := formats[version] + if !ok { + return nil, fmt.Errorf("unknown agent config format %q", version) + } + return format, nil +} + +func parseConfigData(data []byte) (formatter, *configInternal, error) { + i := bytes.IndexByte(data, '\n') + if i == -1 { + return nil, nil, fmt.Errorf("invalid agent config format: %s", string(data)) + } + version, configData := string(data[0:i]), data[i+1:] + if !strings.HasPrefix(version, formatPrefix) { + return nil, nil, fmt.Errorf("malformed agent config format %q", version) + } + version = strings.TrimPrefix(version, formatPrefix) + format, err := getFormatter(version) + if err != nil { + return nil, nil, err + } + config, err := format.unmarshal(configData) + if err != nil { + return nil, nil, err + } + return format, config, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/format_whitebox_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/format_whitebox_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/format_whitebox_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/format_whitebox_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,13 @@ package agent import ( - "io/ioutil" - "path" + "os" + "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -24,6 +26,7 @@ var agentParams = AgentConfigParams{ Tag: "omg", UpgradedToVersion: version.Current.Number, + Jobs: []params.MachineJob{params.JobHostUnits}, Password: "sekrit", CACert: []byte("ca cert"), StateAddresses: []string{"localhost:1234"}, @@ -31,74 +34,77 @@ Nonce: "a nonce", } -func (*formatSuite) TestReadFormatEmptyDir(c *gc.C) { - // Since the previous format didn't have a format file, a missing format - // should return the previous format. Once we are over the hump of - // missing format files, a missing format file should generate an error. - dir := c.MkDir() - format, err := readFormat(dir) - c.Assert(format, gc.Equals, previousFormat) +func newTestConfig(c *gc.C) *configInternal { + params := agentParams + params.DataDir = c.MkDir() + params.LogDir = c.MkDir() + config, err := NewAgentConfig(params) c.Assert(err, gc.IsNil) + return config.(*configInternal) } -func (*formatSuite) TestReadFormat(c *gc.C) { - dir := c.MkDir() - // Make sure that the write adds the carriage return to show that - // this is stripped off for the returned format. - err := ioutil.WriteFile(path.Join(dir, formatFilename), []byte("some format\n"), 0644) - c.Assert(err, gc.IsNil) - format, err := readFormat(dir) - c.Assert(format, gc.Equals, "some format") +func (*formatSuite) TestWriteCommands(c *gc.C) { + config := newTestConfig(c) + commands, err := config.WriteCommands() c.Assert(err, gc.IsNil) + c.Assert(commands, gc.HasLen, 3) + c.Assert(commands[0], gc.Matches, `mkdir -p '\S+/agents/omg'`) + c.Assert(commands[1], gc.Matches, `install -m 600 /dev/null '\S+/agents/omg/agent.conf'`) + c.Assert(commands[2], gc.Matches, `printf '%s\\n' '(.|\n)*' > '\S+/agents/omg/agent.conf'`) } -func (*formatSuite) TestNewFormatter(c *gc.C) { - formatter, err := newFormatter(currentFormat) - c.Assert(formatter, gc.NotNil) +func (*formatSuite) TestWriteAgentConfig(c *gc.C) { + config := newTestConfig(c) + err := config.Write() c.Assert(err, gc.IsNil) - formatter, err = newFormatter(previousFormat) - c.Assert(formatter, gc.NotNil) - c.Assert(err, gc.IsNil) + configPath := ConfigPath(config.DataDir(), config.Tag()) + formatPath := filepath.Join(config.Dir(), legacyFormatFilename) + assertFileExists(c, configPath) + assertFileNotExist(c, formatPath) +} - formatter, err = newFormatter("other") - c.Assert(formatter, gc.IsNil) - c.Assert(err, gc.ErrorMatches, "unknown agent config format") +func (*formatSuite) TestRead(c *gc.C) { + config := newTestConfig(c) + assertWriteAndRead(c, config) } -func (*formatSuite) TestWriteFormat(c *gc.C) { - dir := c.MkDir() - testDir := path.Join(dir, "test") - err := writeFormatFile(testDir, "some format") - c.Assert(err, gc.IsNil) - format, err := readFormat(testDir) - c.Assert(format, gc.Equals, "some format") - c.Assert(err, gc.IsNil) - // Make sure the carriage return is there as it makes catting the file nicer. - content, err := ioutil.ReadFile(path.Join(testDir, formatFilename)) +func (*formatSuite) TestReadWriteStateConfig(c *gc.C) { + stateParams := StateMachineConfigParams{ + AgentConfigParams: agentParams, + StateServerCert: []byte("some special cert"), + StateServerKey: []byte("a special key"), + StatePort: 12345, + APIPort: 23456, + } + stateParams.DataDir = c.MkDir() + stateParams.Values = map[string]string{"foo": "bar", "wibble": "wobble"} + configInterface, err := NewStateMachineConfig(stateParams) c.Assert(err, gc.IsNil) - c.Assert(string(content), gc.Equals, "some format\n") -} + config, ok := configInterface.(*configInternal) + c.Assert(ok, jc.IsTrue) -func (*formatSuite) TestWriteCommandsForFormat(c *gc.C) { - dir := c.MkDir() - testDir := path.Join(dir, "test") - commands := writeCommandsForFormat(testDir, "some format") - c.Assert(commands, gc.HasLen, 3) - c.Assert(commands[0], gc.Matches, `mkdir -p \S+`) - c.Assert(commands[1], gc.Matches, `install -m 644 /dev/null '\S+/format'`) - c.Assert(commands[2], gc.Matches, `printf '%s\\n' '.*' > '\S+/format'`) + assertWriteAndRead(c, config) } -func (*formatSuite) TestReadPreviousFormatWritesNew(c *gc.C) { - config := newTestConfig(c) - - err := previousFormatter.write(config) +func assertWriteAndRead(c *gc.C, config *configInternal) { + err := config.Write() c.Assert(err, gc.IsNil) - - _, err = ReadConf(config.DataDir(), config.Tag()) + configPath := ConfigPath(config.DataDir(), config.Tag()) + readConfig, err := ReadConf(configPath) c.Assert(err, gc.IsNil) - format, err := readFormat(config.Dir()) + c.Assert(readConfig, jc.DeepEquals, config) +} + +func assertFileExists(c *gc.C, path string) { + fileInfo, err := os.Stat(path) c.Assert(err, gc.IsNil) - c.Assert(format, gc.Equals, currentFormat) + c.Assert(fileInfo.Mode().IsRegular(), jc.IsTrue) + c.Assert(fileInfo.Mode().Perm(), gc.Equals, os.FileMode(0600)) + c.Assert(fileInfo.Size(), jc.GreaterThan, 0) +} + +func assertFileNotExist(c *gc.C, path string) { + _, err := os.Stat(path) + c.Assert(err, jc.Satisfies, os.IsNotExist) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/mongo/mongo.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/mongo/mongo.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/mongo/mongo.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/mongo/mongo.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,192 @@ +package mongo + +import ( + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + + "github.com/juju/loggo" + + "launchpad.net/juju-core/upstart" + "launchpad.net/juju-core/utils" +) + +const ( + maxFiles = 65000 + maxProcs = 20000 +) + +var ( + logger = loggo.GetLogger("juju.agent.mongo") + + oldMongoServiceName = "juju-db" + + // JujuMongodPath holds the default path to the juju-specific mongod. + JujuMongodPath = "/usr/lib/juju/bin/mongod" + // MongodbServerPath holds the default path to the generic mongod. + MongodbServerPath = "/usr/bin/mongod" +) + +// MongoPackageForSeries returns the name of the mongo package for the series +// of the machine that it is going to be running on. +func MongoPackageForSeries(series string) string { + switch series { + case "precise", "quantal", "raring", "saucy": + return "mongodb-server" + default: + // trusty and onwards + return "juju-mongodb" + } +} + +// MongodPathForSeries returns the path to the mongod executable for the +// series of the machine that it is going to be running on. +func MongodPathForSeries(series string) string { + if series == "trusty" { + return JujuMongodPath + } + return MongodbServerPath +} + +// MongoPath returns the executable path to be used to run mongod on this +// machine. If the juju-bundled version of mongo exists, it will return that +// path, otherwise it will return the command to run mongod from the path. +func MongodPath() (string, error) { + if _, err := os.Stat(JujuMongodPath); err == nil { + return JujuMongodPath, nil + } + + path, err := exec.LookPath("mongod") + if err != nil { + return "", err + } + return path, nil +} + +// EnsureMongoServer ensures that the correct mongo upstart script is installed +// and running. +// +// This method will remove old versions of the mongo upstart script as necessary +// before installing the new version. +func EnsureMongoServer(dir string, port int) error { + // NOTE: ensure that the right package is installed? + name := makeServiceName(mongoScriptVersion) + // TODO: get the series from somewhere, non trusty values return + // the existing default path. + mongodPath := MongodPathForSeries("some-series") + service, err := MongoUpstartService(name, mongodPath, dir, port) + if err != nil { + return err + } + if service.Installed() { + return nil + } + + if err := removeOldMongoServices(mongoScriptVersion); err != nil { + return err + } + + if err := makeJournalDirs(dir); err != nil { + return err + } + + if err := service.Install(); err != nil { + return fmt.Errorf("failed to install mongo service %q: %v", service.Name, err) + } + return service.Start() +} + +func makeJournalDirs(dir string) error { + journalDir := path.Join(dir, "journal") + + if err := os.MkdirAll(journalDir, 0700); err != nil { + logger.Errorf("failed to make mongo journal dir %s: %v", journalDir, err) + return err + } + + // manually create the prealloc files, since otherwise they get created as 100M files. + zeroes := make([]byte, 64*1024) // should be enough for anyone + for x := 0; x < 3; x++ { + name := fmt.Sprintf("prealloc.%d", x) + filename := filepath.Join(journalDir, name) + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) + if err != nil { + return fmt.Errorf("failed to open mongo prealloc file %q: %v", filename, err) + } + defer f.Close() + for total := 0; total < 1024*1024; { + n, err := f.Write(zeroes) + if err != nil { + return fmt.Errorf("failed to write to mongo prealloc file %q: %v", filename, err) + } + total += n + } + } + return nil +} + +// removeOldMongoServices looks for any old juju mongo upstart scripts and +// removes them. +func removeOldMongoServices(curVersion int) error { + old := upstart.NewService(oldMongoServiceName) + if err := old.StopAndRemove(); err != nil { + logger.Errorf("Failed to remove old mongo upstart service %q: %v", old.Name, err) + return err + } + + // the new formatting for the script name started at version 2 + for x := 2; x < curVersion; x++ { + old := upstart.NewService(makeServiceName(x)) + if err := old.StopAndRemove(); err != nil { + logger.Errorf("Failed to remove old mongo upstart service %q: %v", old.Name, err) + return err + } + } + return nil +} + +func makeServiceName(version int) string { + return fmt.Sprintf("juju-db-v%d", version) +} + +// mongoScriptVersion keeps track of changes to the mongo upstart script. +// Update this version when you update the script that gets installed from +// MongoUpstartService. +const mongoScriptVersion = 2 + +// MongoUpstartService returns the upstart config for the mongo state service. +// +// This method assumes there is a server.pem keyfile in dataDir. +func MongoUpstartService(name, mongodExec, dataDir string, port int) (*upstart.Conf, error) { + + keyFile := path.Join(dataDir, "server.pem") + svc := upstart.NewService(name) + + dbDir := path.Join(dataDir, "db") + + conf := &upstart.Conf{ + Service: *svc, + Desc: "juju state database", + Limit: map[string]string{ + "nofile": fmt.Sprintf("%d %d", maxFiles, maxFiles), + "nproc": fmt.Sprintf("%d %d", maxProcs, maxProcs), + }, + Cmd: mongodExec + + " --auth" + + " --dbpath=" + dbDir + + " --sslOnNormalPorts" + + " --sslPEMKeyFile " + utils.ShQuote(keyFile) + + " --sslPEMKeyPassword ignored" + + " --bind_ip 0.0.0.0" + + " --port " + fmt.Sprint(port) + + " --noprealloc" + + " --syslog" + + " --smallfiles", + // TODO(Nate): uncomment when we commit HA stuff + // + + // " --replSet juju", + } + return conf, nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/mongo/mongo_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/mongo/mongo_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/mongo/mongo_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/mongo/mongo_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,147 @@ +package mongo + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/upstart" +) + +func Test(t *testing.T) { gc.TestingT(t) } + +type MongoSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&MongoSuite{}) + +func (s *MongoSuite) SetUpSuite(c *gc.C) { + testpath := c.MkDir() + s.PatchEnvPathPrepend(testpath) + // mock out the start method so we can fake install services without sudo + start := filepath.Join(testpath, "start") + err := ioutil.WriteFile(start, []byte("#!/bin/bash --norc\nexit 0"), 0755) + c.Assert(err, gc.IsNil) + + s.PatchValue(&upstart.InitDir, c.MkDir()) +} + +func (s *MongoSuite) TestJujuMongodPath(c *gc.C) { + d := c.MkDir() + defer os.RemoveAll(d) + mongoPath := filepath.Join(d, "mongod") + s.PatchValue(&JujuMongodPath, mongoPath) + + err := ioutil.WriteFile(mongoPath, nil, 0777) + c.Assert(err, gc.IsNil) + + obtained, err := MongodPath() + c.Check(err, gc.IsNil) + c.Check(obtained, gc.Equals, mongoPath) +} + +func (s *MongoSuite) TestDefaultMongodPath(c *gc.C) { + s.PatchValue(&JujuMongodPath, "/not/going/to/exist/mongod") + + dir := c.MkDir() + s.PatchEnvPathPrepend(dir) + filename := filepath.Join(dir, "mongod") + err := ioutil.WriteFile(filename, nil, 0777) + c.Assert(err, gc.IsNil) + + obtained, err := MongodPath() + c.Check(err, gc.IsNil) + c.Check(obtained, gc.Equals, filename) +} + +func (s *MongoSuite) TestRemoveOldMongoServices(c *gc.C) { + s.PatchValue(&oldMongoServiceName, "someNameThatShouldntExist") + + // Make fake old services. + // We defer the removes manually just in case the test fails, we don't leave + // junk behind. + conf := makeService(oldMongoServiceName, c) + defer conf.Remove() + conf2 := makeService(makeServiceName(2), c) + defer conf2.Remove() + conf3 := makeService(makeServiceName(3), c) + defer conf3.Remove() + + // Remove with current version = 4, which should remove all previous + // versions plus the old service name. + err := removeOldMongoServices(4) + c.Assert(err, gc.IsNil) + + c.Assert(conf.Installed(), jc.IsFalse) + c.Assert(conf2.Installed(), jc.IsFalse) + c.Assert(conf3.Installed(), jc.IsFalse) +} + +func (s *MongoSuite) TestMakeJournalDirs(c *gc.C) { + dir := c.MkDir() + err := makeJournalDirs(dir) + c.Assert(err, gc.IsNil) + + testJournalDirs(dir, c) +} + +func testJournalDirs(dir string, c *gc.C) { + journalDir := path.Join(dir, "journal") + + c.Check(journalDir, jc.IsDirectory) + info, err := os.Stat(filepath.Join(journalDir, "prealloc.0")) + c.Check(err, gc.IsNil) + + size := int64(1024 * 1024) + + c.Check(info.Size(), gc.Equals, size) + info, err = os.Stat(filepath.Join(journalDir, "prealloc.1")) + c.Check(err, gc.IsNil) + c.Check(info.Size(), gc.Equals, size) + info, err = os.Stat(filepath.Join(journalDir, "prealloc.2")) + c.Check(err, gc.IsNil) + c.Check(info.Size(), gc.Equals, size) + +} + +func (s *MongoSuite) TestEnsureMongoServer(c *gc.C) { + dir := c.MkDir() + port := 25252 + + oldsvc := makeService(oldMongoServiceName, c) + defer oldsvc.Remove() + + err := EnsureMongoServer(dir, port) + c.Assert(err, gc.IsNil) + mongodPath := MongodPathForSeries("some-series") + svc, err := MongoUpstartService(makeServiceName(mongoScriptVersion), mongodPath, dir, port) + c.Assert(err, gc.IsNil) + defer svc.Remove() + + testJournalDirs(dir, c) + c.Check(oldsvc.Installed(), jc.IsFalse) + c.Check(svc.Installed(), jc.IsTrue) + + // now check we can call it multiple times without error + err = EnsureMongoServer(dir, port) + c.Assert(err, gc.IsNil) + +} + +func makeService(name string, c *gc.C) *upstart.Conf { + conf := &upstart.Conf{ + Desc: "foo", + Service: *upstart.NewService(name), + Cmd: "echo hi", + } + err := conf.Install() + c.Assert(err, gc.IsNil) + return conf +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/diskmanager_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/diskmanager_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/diskmanager_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/diskmanager_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" + "os" "path/filepath" gc "launchpad.net/gocheck" @@ -101,4 +102,9 @@ gotTools, err := s.manager.ReadTools(t.Version) c.Assert(err, gc.IsNil) c.Assert(*gotTools, gc.Equals, *t) + // Make sure that the tools directory is readable by the ubuntu user (for + // juju-run) + info, err := os.Stat(dir) + c.Assert(err, gc.IsNil) + c.Assert(info.Mode().Perm(), gc.Equals, os.FileMode(0755)) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/toolsdir.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/toolsdir.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/toolsdir.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/toolsdir.go 2014-03-20 12:52:38.000000000 +0000 @@ -113,6 +113,13 @@ return err } + // The tempdir is created with 0700, so we need to make it more + // accessable for juju-run. + err = os.Chmod(dir, 0755) + if err != nil { + return err + } + err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version)) // If we've failed to rename the directory, it may be because // the directory already exists - if ReadTools succeeds, we diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/tools.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/tools.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/tools.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/tools.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "io" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/tools_test.go juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/tools_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/agent/tools/tools_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/agent/tools/tools_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -39,7 +39,7 @@ // resulting slice has that prefix removed to keep the output short. c.Assert(testbase.FindJujuCoreImports(c, "launchpad.net/juju-core/agent/tools"), gc.DeepEquals, - []string{"tools", "utils/set", "version"}) + []string{"juju/arch", "tools", "utils/set", "version"}) } const toolsFile = "downloaded-tools.txt" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/bundle.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/bundle.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/bundle.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/bundle.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,7 +12,9 @@ "os" "path/filepath" "strconv" - "strings" + + "launchpad.net/juju-core/utils/set" + ziputil "launchpad.net/juju-core/utils/zip" ) // The Bundle type encapsulates access to data and operations @@ -140,125 +142,102 @@ return b.config } -// ExpandTo expands the charm bundle into dir, creating it if necessary. -// If any errors occur during the expansion procedure, the process will -// continue. Only the last error found is returned. -func (b *Bundle) ExpandTo(dir string) (err error) { - // If we have a Path, reopen the file. Otherwise, try to use - // the original ReaderAt. - r := b.r - size := b.size - if b.Path != "" { - f, err := os.Open(b.Path) - if err != nil { - return err - } - defer f.Close() - fi, err := f.Stat() +type zipReadCloser struct { + io.Closer + *zip.Reader +} + +// zipOpen returns a zipReadCloser. +func (b *Bundle) zipOpen() (*zipReadCloser, error) { + // If we don't have a Path, try to use the original ReaderAt. + if b.Path == "" { + r, err := zip.NewReader(b.r, b.size) if err != nil { - return err + return nil, err } - r = f - size = fi.Size() + return &zipReadCloser{Closer: ioutil.NopCloser(nil), Reader: r}, nil } - - zipr, err := zip.NewReader(r, size) + f, err := os.Open(b.Path) if err != nil { - return err - } - - hooks := b.meta.Hooks() - var lasterr error - for _, zfile := range zipr.File { - if err := b.expand(hooks, dir, zfile); err != nil { - lasterr = err - } + return nil, err } - - revFile, err := os.Create(filepath.Join(dir, "revision")) + fi, err := f.Stat() if err != nil { - return err + f.Close() + return nil, err } - _, err = revFile.Write([]byte(strconv.Itoa(b.revision))) - revFile.Close() + r, err := zip.NewReader(f, fi.Size()) if err != nil { - return err + f.Close() + return nil, err } - return lasterr + return &zipReadCloser{Closer: f, Reader: r}, nil } -// expand unpacks a charm's zip file into the given directory. -// The hooks map holds all the possible hook names in the -// charm. -func (b *Bundle) expand(hooks map[string]bool, dir string, zfile *zip.File) error { - cleanName := filepath.Clean(zfile.Name) - if cleanName == "revision" { - return nil - } - - r, err := zfile.Open() +// Manifest returns a set of the charm's contents. +func (b *Bundle) Manifest() (set.Strings, error) { + zipr, err := b.zipOpen() if err != nil { - return err + return set.NewStrings(), err } - defer r.Close() - - mode := zfile.Mode() - path := filepath.Join(dir, cleanName) - if strings.HasSuffix(zfile.Name, "/") || mode&os.ModeDir != 0 { - err = os.MkdirAll(path, mode&0777) - if err != nil { - return err - } - return nil + defer zipr.Close() + paths, err := ziputil.Find(zipr.Reader, "*") + if err != nil { + return set.NewStrings(), err } + manifest := set.NewStrings(paths...) + // We always write out a revision file, even if there isn't one in the + // bundle; and we always strip ".", because that's sometimes not present. + manifest.Add("revision") + manifest.Remove(".") + return manifest, nil +} - base, _ := filepath.Split(path) - err = os.MkdirAll(base, 0755) +// ExpandTo expands the charm bundle into dir, creating it if necessary. +// If any errors occur during the expansion procedure, the process will +// abort. +func (b *Bundle) ExpandTo(dir string) (err error) { + zipr, err := b.zipOpen() if err != nil { return err } - - if mode&os.ModeSymlink != 0 { - data, err := ioutil.ReadAll(r) - if err != nil { - return err - } - target := string(data) - if err := checkSymlinkTarget(dir, cleanName, target); err != nil { - return err - } - return os.Symlink(target, path) + defer zipr.Close() + if err := ziputil.ExtractAll(zipr.Reader, dir); err != nil { + return err } - if filepath.Dir(cleanName) == "hooks" { - hookName := filepath.Base(cleanName) - if _, ok := hooks[hookName]; mode&os.ModeType == 0 && ok { - // Set all hooks executable (by owner) - mode = mode | 0100 + hooksDir := filepath.Join(dir, "hooks") + fixHook := fixHookFunc(hooksDir, b.meta.Hooks()) + if err := filepath.Walk(hooksDir, fixHook); err != nil { + if !os.IsNotExist(err) { + return err } } - - if err := checkFileType(cleanName, mode); err != nil { - return err - } - - f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode&0777) + revFile, err := os.Create(filepath.Join(dir, "revision")) if err != nil { return err } - _, err = io.Copy(f, r) - f.Close() + _, err = revFile.Write([]byte(strconv.Itoa(b.revision))) + revFile.Close() return err } -func checkSymlinkTarget(basedir, symlink, target string) error { - if filepath.IsAbs(target) { - return fmt.Errorf("symlink %q is absolute: %q", symlink, target) - } - p := filepath.Join(filepath.Dir(symlink), target) - if p == ".." || strings.HasPrefix(p, "../") { - return fmt.Errorf("symlink %q links out of charm: %q", symlink, target) +// fixHookFunc returns a WalkFunc that makes sure hooks are owner-executable. +func fixHookFunc(hooksDir string, hookNames map[string]bool) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + mode := info.Mode() + if path != hooksDir && mode.IsDir() { + return filepath.SkipDir + } + if name := filepath.Base(path); hookNames[name] { + if mode&0100 == 0 { + return os.Chmod(path, mode|0100) + } + } + return nil } - return nil } // FWIW, being able to do this is awesome. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/bundle_test.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/bundle_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/bundle_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/bundle_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,11 +14,13 @@ "strconv" "syscall" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/utils/set" ) type BundleSuite struct { @@ -32,6 +34,17 @@ s.bundlePath = testing.Charms.BundlePath(c.MkDir(), "dummy") } +var dummyManifest = []string{ + "config.yaml", + "empty", + "hooks", + "hooks/install", + "metadata.yaml", + "revision", + "src", + "src/hello.c", +} + func (s *BundleSuite) TestReadBundle(c *gc.C) { bundle, err := charm.ReadBundle(s.bundlePath) c.Assert(err, gc.IsNil) @@ -57,6 +70,42 @@ checkDummy(c, bundle, "") } +func (s *BundleSuite) TestManifest(c *gc.C) { + bundle, err := charm.ReadBundle(s.bundlePath) + c.Assert(err, gc.IsNil) + manifest, err := bundle.Manifest() + c.Assert(err, gc.IsNil) + c.Assert(manifest, jc.DeepEquals, set.NewStrings(dummyManifest...)) +} + +func (s *BundleSuite) TestManifestNoRevision(c *gc.C) { + bundle, err := charm.ReadBundle(s.bundlePath) + c.Assert(err, gc.IsNil) + dirPath := c.MkDir() + err = bundle.ExpandTo(dirPath) + c.Assert(err, gc.IsNil) + err = os.Remove(filepath.Join(dirPath, "revision")) + c.Assert(err, gc.IsNil) + + bundle = extBundleDir(c, dirPath) + manifest, err := bundle.Manifest() + c.Assert(err, gc.IsNil) + c.Assert(manifest, gc.DeepEquals, set.NewStrings(dummyManifest...)) +} + +func (s *BundleSuite) TestManifestSymlink(c *gc.C) { + srcPath := testing.Charms.ClonedDirPath(c.MkDir(), "dummy") + if err := os.Symlink("../target", filepath.Join(srcPath, "hooks/symlink")); err != nil { + c.Skip("cannot symlink") + } + expected := append([]string{"hooks/symlink"}, dummyManifest...) + + bundle := bundleDir(c, srcPath) + manifest, err := bundle.Manifest() + c.Assert(err, gc.IsNil) + c.Assert(manifest, gc.DeepEquals, set.NewStrings(expected...)) +} + func (s *BundleSuite) TestExpandTo(c *gc.C) { bundle, err := charm.ReadBundle(s.bundlePath) c.Assert(err, gc.IsNil) @@ -153,15 +202,9 @@ } // Bundle and extract the charm to a new directory. - dir, err := charm.ReadDir(srcPath) - c.Assert(err, gc.IsNil) - buf := new(bytes.Buffer) - err = dir.BundleTo(buf) - c.Assert(err, gc.IsNil) - bundle, err := charm.ReadBundleBytes(buf.Bytes()) - c.Assert(err, gc.IsNil) + bundle := bundleDir(c, srcPath) path := c.MkDir() - err = bundle.ExpandTo(path) + err := bundle.ExpandTo(path) c.Assert(err, gc.IsNil) // Check sensible file modes once round-tripped. @@ -194,8 +237,7 @@ err := os.Remove(revPath) c.Assert(err, gc.IsNil) - bundle, err := charm.ReadBundle(extBundleDir(c, charmDir)) - c.Assert(err, gc.IsNil) + bundle := extBundleDir(c, charmDir) c.Assert(bundle.Revision(), gc.Equals, 0) // Missing revision file with old revision in metadata @@ -204,15 +246,15 @@ _, err = file.Write([]byte("\nrevision: 1234\n")) c.Assert(err, gc.IsNil) - bundle, err = charm.ReadBundle(extBundleDir(c, charmDir)) - c.Assert(err, gc.IsNil) + bundle = extBundleDir(c, charmDir) c.Assert(bundle.Revision(), gc.Equals, 1234) // Revision file with bad content err = ioutil.WriteFile(revPath, []byte("garbage"), 0666) c.Assert(err, gc.IsNil) - bundle, err = charm.ReadBundle(extBundleDir(c, charmDir)) + path := extBundleDirPath(c, charmDir) + bundle, err = charm.ReadBundle(path) c.Assert(err, gc.ErrorMatches, "invalid revision file") c.Assert(bundle, gc.IsNil) } @@ -242,30 +284,48 @@ err := os.Symlink("../../target", badLink) c.Assert(err, gc.IsNil) - bundle, err := charm.ReadBundle(extBundleDir(c, charmDir)) + bundle := extBundleDir(c, charmDir) c.Assert(err, gc.IsNil) path := filepath.Join(c.MkDir(), "charm") err = bundle.ExpandTo(path) - c.Assert(err, gc.ErrorMatches, `symlink "hooks/badlink" links out of charm: "../../target"`) + c.Assert(err, gc.ErrorMatches, `cannot extract "hooks/badlink": symlink "../../target" leads out of scope`) // Symlink targeting an absolute path. os.Remove(badLink) err = os.Symlink("/target", badLink) c.Assert(err, gc.IsNil) - bundle, err = charm.ReadBundle(extBundleDir(c, charmDir)) + bundle = extBundleDir(c, charmDir) c.Assert(err, gc.IsNil) path = filepath.Join(c.MkDir(), "charm") err = bundle.ExpandTo(path) - c.Assert(err, gc.ErrorMatches, `symlink "hooks/badlink" is absolute: "/target"`) + c.Assert(err, gc.ErrorMatches, `cannot extract "hooks/badlink": symlink "/target" is absolute`) } -func extBundleDir(c *gc.C, dirpath string) (path string) { - path = filepath.Join(c.MkDir(), "bundle.charm") +func extBundleDirPath(c *gc.C, dirpath string) string { + path := filepath.Join(c.MkDir(), "bundle.charm") cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip --fifo --symlinks -r %s .", dirpath, path)) output, err := cmd.CombinedOutput() c.Assert(err, gc.IsNil, gc.Commentf("Command output: %s", output)) return path } + +func extBundleDir(c *gc.C, dirpath string) *charm.Bundle { + path := extBundleDirPath(c, dirpath) + bundle, err := charm.ReadBundle(path) + c.Assert(err, gc.IsNil) + return bundle +} + +func bundleDir(c *gc.C, dirpath string) *charm.Bundle { + dir, err := charm.ReadDir(dirpath) + c.Assert(err, gc.IsNil) + buf := new(bytes.Buffer) + err = dir.BundleTo(buf) + c.Assert(err, gc.IsNil) + bundle, err := charm.ReadBundleBytes(buf.Bytes()) + c.Assert(err, gc.IsNil) + return bundle +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/dir.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/dir.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/dir.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/dir.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,6 +12,7 @@ "os" "path/filepath" "strconv" + "strings" "syscall" "launchpad.net/juju-core/log" @@ -219,6 +220,17 @@ return err } +func checkSymlinkTarget(basedir, symlink, target string) error { + if filepath.IsAbs(target) { + return fmt.Errorf("symlink %q is absolute: %q", symlink, target) + } + p := filepath.Join(filepath.Dir(symlink), target) + if p == ".." || strings.HasPrefix(p, "../") { + return fmt.Errorf("symlink %q links out of charm: %q", symlink, target) + } + return nil +} + func checkFileType(path string, mode os.FileMode) error { e := "file has an unknown type: %q" switch mode & os.ModeType { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/repo.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/repo.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo.go 2014-03-20 12:52:38.000000000 +0000 @@ -87,6 +87,7 @@ BaseURL string authAttrs string // a list of attr=value pairs, comma separated jujuAttrs string // a list of attr=value pairs, comma separated + testMode bool } var _ Repository = (*CharmStore)(nil) @@ -101,6 +102,14 @@ return &authCS } +// WithTestMode returns a Repository where testMode is set to value passed to +// this method. +func (s *CharmStore) WithTestMode(testMode bool) Repository { + newRepo := *s + newRepo.testMode = testMode + return &newRepo +} + // WithJujuAttrs returns a Repository with the Juju metadata attributes set. // jujuAttrs is a list of attr=value pairs. func (s *CharmStore) WithJujuAttrs(jujuAttrs string) Repository { @@ -131,11 +140,14 @@ // Info returns details for all the specified charms in the charm store. func (s *CharmStore) Info(curls ...*URL) ([]*InfoResponse, error) { baseURL := s.BaseURL + "/charm-info?" - charmSnippets := make([]string, len(curls)) + queryParams := make([]string, len(curls), len(curls)+1) for i, curl := range curls { - charmSnippets[i] = "charms=" + url.QueryEscape(curl.String()) + queryParams[i] = "charms=" + url.QueryEscape(curl.String()) } - resp, err := s.get(baseURL + strings.Join(charmSnippets, "&")) + if s.testMode { + queryParams = append(queryParams, "stats=0") + } + resp, err := s.get(baseURL + strings.Join(queryParams, "&")) if err != nil { if url_error, ok := err.(*url.Error); ok { switch url_error.Err.(type) { @@ -335,7 +347,11 @@ } path := filepath.Join(CacheDir, Quote(curl.String())+".charm") if verify(path, digest) != nil { - resp, err := s.get(s.BaseURL + "/charm/" + url.QueryEscape(curl.Path())) + store_url := s.BaseURL + "/charm/" + url.QueryEscape(curl.Path()) + if s.testMode { + store_url = store_url + "?stats=0" + } + resp, err := s.get(store_url) if err != nil { return nil, err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/repo_test.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/repo_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -43,6 +43,9 @@ s.server.Downloads = nil s.server.Authorizations = nil s.server.Metadata = nil + s.server.DownloadsNoStats = nil + s.server.InfoRequestCount = 0 + s.server.InfoRequestCountNoStats = 0 } // Uses the TearDownTest from testbase.LoggingSuite @@ -144,6 +147,29 @@ s.assertCached(c, revCharmURL) } +func (s *StoreSuite) TestGetTestModeFlag(c *gc.C) { + base := "cs:series/good-12" + charmURL := charm.MustParseURL(base) + ch, err := s.store.Get(charmURL) + c.Assert(err, gc.IsNil) + c.Assert(ch, gc.NotNil) + c.Assert(s.server.Downloads, gc.DeepEquals, []*charm.URL{charmURL}) + c.Assert(s.server.DownloadsNoStats, gc.IsNil) + c.Assert(s.server.InfoRequestCount, gc.Equals, 1) + c.Assert(s.server.InfoRequestCountNoStats, gc.Equals, 0) + + storeInTestMode := s.store.WithTestMode(true) + other := "cs:series/good-23" + otherURL := charm.MustParseURL(other) + ch, err = storeInTestMode.Get(otherURL) + c.Assert(err, gc.IsNil) + c.Assert(ch, gc.NotNil) + c.Assert(s.server.Downloads, gc.DeepEquals, []*charm.URL{charmURL}) + c.Assert(s.server.DownloadsNoStats, gc.DeepEquals, []*charm.URL{otherURL}) + c.Assert(s.server.InfoRequestCount, gc.Equals, 1) + c.Assert(s.server.InfoRequestCountNoStats, gc.Equals, 1) +} + // The following tests cover the low-level CharmStore-specific API. func (s *StoreSuite) TestInfo(c *gc.C) { @@ -187,6 +213,21 @@ c.Assert(info[0].Warnings, gc.DeepEquals, []string{"foolishness"}) } +func (s *StoreSuite) TestInfoTestModeFlag(c *gc.C) { + charmURL := charm.MustParseURL("cs:series/good") + _, err := s.store.Info(charmURL) + c.Assert(err, gc.IsNil) + c.Assert(s.server.InfoRequestCount, gc.Equals, 1) + c.Assert(s.server.InfoRequestCountNoStats, gc.Equals, 0) + + storeInTestMode, ok := s.store.WithTestMode(true).(*charm.CharmStore) + c.Assert(ok, gc.Equals, true) + _, err = storeInTestMode.Info(charmURL) + c.Assert(err, gc.IsNil) + c.Assert(s.server.InfoRequestCount, gc.Equals, 1) + c.Assert(s.server.InfoRequestCountNoStats, gc.Equals, 1) +} + func (s *StoreSuite) TestInfoDNSError(c *gc.C) { store := charm.NewStore("http://0.1.2.3") charmURL := charm.MustParseURL("cs:series/good") @@ -238,7 +279,7 @@ func (s *StoreSuite) TestAuthorization(c *gc.C) { config := testing.CustomEnvironConfig(c, testing.Attrs{"charm-store-auth": "token=value"}) - store := env_config.AuthorizeCharmRepo(s.store, config) + store := env_config.SpecializeCharmRepo(s.store, config) base := "cs:series/good" charmURL := charm.MustParseURL(base) @@ -252,7 +293,7 @@ func (s *StoreSuite) TestNilAuthorization(c *gc.C) { config := testing.EnvironConfig(c) - store := env_config.AuthorizeCharmRepo(s.store, config) + store := env_config.SpecializeCharmRepo(s.store, config) base := "cs:series/good" charmURL := charm.MustParseURL(base) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/charm/testing/mockstore.go juju-core-1.17.6/src/launchpad.net/juju-core/charm/testing/mockstore.go --- juju-core-1.17.4/src/launchpad.net/juju-core/charm/testing/mockstore.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/charm/testing/mockstore.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,7 @@ "strconv" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -25,13 +25,16 @@ // MockStore provides a mock charm store implementation useful when testing. type MockStore struct { - mux *http.ServeMux - listener net.Listener - bundleBytes []byte - bundleSha256 string - Downloads []*charm.URL - Authorizations []string - Metadata []string + mux *http.ServeMux + listener net.Listener + bundleBytes []byte + bundleSha256 string + Downloads []*charm.URL + DownloadsNoStats []*charm.URL + Authorizations []string + Metadata []string + InfoRequestCount int + InfoRequestCountNoStats int charms map[string]int } @@ -85,6 +88,12 @@ } r.ParseForm() + if r.Form.Get("stats") == "0" { + s.InfoRequestCountNoStats += 1 + } else { + s.InfoRequestCount += 1 + } + response := map[string]*charm.InfoResponse{} for _, url := range r.Form["charms"] { cr := &charm.InfoResponse{} @@ -170,7 +179,13 @@ func (s *MockStore) serveCharm(w http.ResponseWriter, r *http.Request) { charmURL := charm.MustParseURL("cs:" + r.URL.Path[len("/charm/"):]) - s.Downloads = append(s.Downloads, charmURL) + + r.ParseForm() + if r.Form.Get("stats") == "0" { + s.DownloadsNoStats = append(s.DownloadsNoStats, charmURL) + } else { + s.Downloads = append(s.Downloads, charmURL) + } if auth := r.Header.Get("Authorization"); auth != "" { s.Authorizations = append(s.Authorizations, auth) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/options.go juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/options.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/options.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/options.go 2014-03-20 12:52:38.000000000 +0000 @@ -346,7 +346,6 @@ // puppet // resizefs // rightscale_userdata -// rsyslog // scripts_per_boot // scripts_per_instance // scripts_per_once diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/sshinit/configure.go juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/sshinit/configure.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "io" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/utils" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,6 +15,7 @@ envcloudinit "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" envtools "launchpad.net/juju-core/environs/tools" + "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" @@ -55,8 +56,10 @@ var mcfg *envcloudinit.MachineConfig if stateServer { mcfg = environs.NewBootstrapMachineConfig("http://whatever/dotcom", "private-key") + mcfg.Jobs = []params.MachineJob{params.JobManageEnviron, params.JobHostUnits} } else { mcfg = environs.NewMachineConfig("0", "ya", nil, nil) + mcfg.Jobs = []params.MachineJob{params.JobHostUnits} } mcfg.Tools = &tools.Tools{ Version: vers, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/cmd_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/cmd_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,11 +8,11 @@ "net/http" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type CmdSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/addmachine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/addmachine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/addmachine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/addmachine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "strconv" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -14,7 +15,6 @@ jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type AddMachineSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/authorisedkeys_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/authorisedkeys_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/authorisedkeys_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/authorisedkeys_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,6 @@ jujutesting "launchpad.net/juju-core/juju/testing" keymanagerserver "launchpad.net/juju-core/state/apiserver/keymanager" keymanagertesting "launchpad.net/juju-core/state/apiserver/keymanager/testing" - statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" sshtesting "launchpad.net/juju-core/utils/ssh/testing" @@ -103,7 +102,7 @@ func (s *keySuiteBase) setAuthorisedKeys(c *gc.C, keys ...string) { keyString := strings.Join(keys, "\n") - err := statetesting.UpdateConfig(s.State, map[string]interface{}{"authorized-keys": keyString}) + err := s.State.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keyString}, nil, nil) c.Assert(err, gc.IsNil) envConfig, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/bootstrap.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/bootstrap.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,17 +13,10 @@ "launchpad.net/juju-core/charm" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/constraints" - "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/bootstrap" - "launchpad.net/juju-core/environs/config" - "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/environs/imagemetadata" - "launchpad.net/juju-core/environs/sync" "launchpad.net/juju-core/environs/tools" - "launchpad.net/juju-core/errors" "launchpad.net/juju-core/provider" - "launchpad.net/juju-core/utils/set" - "launchpad.net/juju-core/version" ) const bootstrapDoc = ` @@ -95,34 +88,15 @@ return cmd.CheckEmpty(args) } -func destroyPreparedEnviron(env environs.Environ, store configstore.Storage, err *error, action string) { - if *err == nil { - return - } - if err := environs.Destroy(env, store); err != nil { - logger.Errorf("%s failed, and the environment could not be destroyed: %v", action, err) - } -} - // Run connects to the environment specified on the command line and bootstraps // a juju in that environment if none already exists. If there is as yet no environments.yaml file, // the user is informed how to create one. func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) { - store, err := configstore.Default() + environ, cleanup, err := environFromName(ctx, c.EnvName, &resultErr, "Bootstrap") if err != nil { return err } - var existing bool - if _, err := store.ReadInfo(c.EnvName); !errors.IsNotFoundError(err) { - existing = true - } - environ, err := environs.PrepareFromName(c.EnvName, ctx, store) - if err != nil { - return err - } - if !existing { - defer destroyPreparedEnviron(environ, store, &resultErr, "Bootstrap") - } + defer cleanup() if err := bootstrap.EnsureNotBootstrapped(environ); err != nil { return err } @@ -148,7 +122,7 @@ c.UploadTools = true } if c.UploadTools { - err = c.uploadTools(environ) + err = bootstrap.UploadTools(environ, c.Constraints.Arch, true, c.Series...) if err != nil { return err } @@ -156,28 +130,6 @@ return bootstrap.Bootstrap(ctx, environ, c.Constraints) } -func (c *BootstrapCommand) uploadTools(environ environs.Environ) error { - // Force version.Current, for consistency with subsequent upgrade-juju - // (see UpgradeJujuCommand). - forceVersion := uploadVersion(version.Current.Number, nil) - cfg := environ.Config() - series := getUploadSeries(cfg, c.Series) - agenttools, err := sync.Upload(environ.Storage(), &forceVersion, series...) - if err != nil { - return err - } - cfg, err = cfg.Apply(map[string]interface{}{ - "agent-version": agenttools.Version.Number.String(), - }) - if err == nil { - err = environ.SetConfig(cfg) - } - if err != nil { - return fmt.Errorf("failed to update environment configuration: %v", err) - } - return nil -} - type seriesVar struct { target *[]string } @@ -196,16 +148,3 @@ func (v seriesVar) String() string { return strings.Join(*v.target, ",") } - -// getUploadSeries returns the supplied series with duplicates removed if -// non-empty; otherwise it returns a default list of series we should -// probably upload, based on cfg. -func getUploadSeries(cfg *config.Config, series []string) []string { - unique := set.NewStrings(series...) - if unique.IsEmpty() { - unique.Add(version.Current.Series) - unique.Add(config.DefaultSeries) - unique.Add(cfg.DefaultSeries()) - } - return unique.Values() -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,8 @@ "fmt" "strings" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" @@ -24,6 +26,7 @@ envtools "launchpad.net/juju-core/environs/tools" ttesting "launchpad.net/juju-core/environs/tools/testing" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/provider/dummy" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" @@ -79,27 +82,30 @@ addVersionToSource bool } +var noToolsAvailableMessage = "cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*" +var toolsNotFoundMessage = "cannot find bootstrap tools: tools not found" + var bootstrapRetryTests = []bootstrapRetryTest{{ - info: "no tools uploaded, first check has no retries; no matching binary in source; sync fails with no second attempt", + info: "no tools uploaded, first check has no retries; no matching binary in source; no second attempt", expectedAllowRetry: []bool{false}, - err: "cannot find bootstrap tools: no matching tools available", + err: noToolsAvailableMessage, version: "1.16.0-precise-amd64", }, { - info: "no tools uploaded, first check has no retries; matching binary in source; check after sync has retries", + info: "no tools uploaded, first check has no retries; matching binary in source; check after upload has retries", expectedAllowRetry: []bool{false, true}, - err: "cannot find bootstrap tools: tools not found", - version: "1.16.0-precise-amd64", + err: toolsNotFoundMessage, + version: "1.17.0-precise-amd64", // dev version to force upload addVersionToSource: true, }, { info: "no tools uploaded, first check has no retries; no matching binary in source; check after upload has retries", expectedAllowRetry: []bool{false, true}, - err: "cannot find bootstrap tools: tools not found", + err: toolsNotFoundMessage, version: "1.15.1-precise-amd64", // dev version to force upload }, { info: "new tools uploaded, so we want to allow retries to give them a chance at showing up", args: []string{"--upload-tools"}, expectedAllowRetry: []bool{true}, - err: "cannot find bootstrap tools: no matching tools available", + err: noToolsAvailableMessage, }} // Test test checks that bootstrap calls FindTools with the expected allowRetry flag. @@ -114,8 +120,7 @@ toolsVersions := envtesting.VAll if test.version != "" { testVersion := version.MustParseBinary(test.version) - restore := testbase.PatchValue(&version.Current, testVersion) - defer restore() + s.PatchValue(&version.Current, testVersion) if test.addVersionToSource { toolsVersions = append([]version.Binary{}, toolsVersions...) toolsVersions = append(toolsVersions, testVersion) @@ -139,7 +144,32 @@ _, errc := runCommand(nullContext(), new(BootstrapCommand), test.args...) err := <-errc c.Check(findToolsRetryValues, gc.DeepEquals, test.expectedAllowRetry) - c.Check(err, gc.ErrorMatches, test.err) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Check(stripped, gc.Matches, test.err) +} + +// mockUploadTools simulates the effect of tools.Upload, but skips the time- +// consuming build from source. +// TODO(fwereade) better factor agent/tools such that build logic is +// exposed and can itself be neatly mocked? +func mockUploadTools(stor storage.Storage, forceVersion *version.Number, series ...string) (*coretools.Tools, error) { + vers := version.Current + if forceVersion != nil { + vers.Number = *forceVersion + } + versions := []version.Binary{vers} + for _, series := range series { + if series != version.Current.Series { + newVers := vers + newVers.Series = series + versions = append(versions, newVers) + } + } + agentTools, err := envtesting.UploadFakeToolsVersions(stor, versions...) + if err != nil { + return nil, err + } + return agentTools[0], nil } func (s *BootstrapSuite) TestTest(c *gc.C) { @@ -161,6 +191,7 @@ // will be uploaded before running the test. uploads []string constraints constraints.Value + hostArch string } func (test bootstrapTest) run(c *gc.C) { @@ -175,6 +206,14 @@ defer func() { version.Current = origVersion }() } + if test.hostArch != "" { + origVersion := arch.HostArch + arch.HostArch = func() string { + return test.hostArch + } + defer func() { arch.HostArch = origVersion }() + } + uploadCount := len(test.uploads) if uploadCount == 0 { usefulVersion := version.Current @@ -184,6 +223,17 @@ // Run command and check for uploads. opc, errc := runCommand(nullContext(), new(BootstrapCommand), test.args...) + // Check for remaining operations/errors. + if test.err != "" { + err := <-errc + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Check(stripped, gc.Matches, test.err) + return + } + if !c.Check(<-errc, gc.IsNil) { + return + } + if uploadCount > 0 { for i := 0; i < uploadCount; i++ { c.Check((<-opc).(dummy.OpPutFile).Env, gc.Equals, "peckham") @@ -201,15 +251,6 @@ c.Check(found, gc.Equals, true) } } - - // Check for remaining operations/errors. - if test.err != "" { - c.Check(<-errc, gc.ErrorMatches, test.err) - return - } - if !c.Check(<-errc, gc.IsNil) { - return - } if len(test.uploads) > 0 { indexFile := (<-opc).(dummy.OpPutFile) c.Check(indexFile.FileName, gc.Equals, "tools/streams/v1/index.json") @@ -276,6 +317,17 @@ "1.2.3.1-precise-amd64", // from environs/config.DefaultSeries }, }, { + info: "--upload-tools uses arch from constraint if it matches current version", + version: "1.3.3-saucy-ppc64", + hostArch: "ppc64", + args: []string{"--upload-tools", "--constraints", "arch=ppc64"}, + uploads: []string{ + "1.3.3.1-saucy-ppc64", // from version.Current + "1.3.3.1-raring-ppc64", // from env.Config().DefaultSeries() + "1.3.3.1-precise-ppc64", // from environs/config.DefaultSeries + }, + constraints: constraints.MustParse("arch=ppc64"), +}, { info: "--upload-tools only uploads each file once", version: "1.2.3-precise-amd64", args: []string{"--upload-tools"}, @@ -289,6 +341,17 @@ args: []string{"--upload-tools", "--series", "ping,ping,pong"}, err: `invalid series "ping"`, }, { + info: "--upload-tools rejects mismatched arch", + version: "1.3.3-saucy-amd64", + args: []string{"--upload-tools", "--constraints", "arch=ppc64"}, + err: `cannot build tools for "ppc64" using a machine running on "amd64"`, +}, { + info: "--upload-tools rejects non-supported arch", + version: "1.3.3-saucy-arm64", + hostArch: "arm64", + args: []string{"--upload-tools"}, + err: `environment "peckham" of type dummy does not support instances running on "arm64"`, +}, { info: "--upload-tools always bumps build number", version: "1.2.3.4-raring-amd64", args: []string{"--upload-tools"}, @@ -315,6 +378,27 @@ c.Check(coretesting.Stdout(ctx2), gc.Equals, "") } +func (s *BootstrapSuite) TestBootstrapJenvWarning(c *gc.C) { + env, fake := makeEmptyFakeHome(c) + defer fake.Restore() + defaultSeriesVersion := version.Current + defaultSeriesVersion.Series = env.Config().DefaultSeries() + + store, err := configstore.Default() + c.Assert(err, gc.IsNil) + ctx := coretesting.Context(c) + environs.PrepareFromName("peckham", ctx, store) + + logger := "jenv.warning.test" + testWriter := &loggo.TestWriter{} + loggo.RegisterWriter(logger, testWriter, loggo.WARNING) + defer loggo.RemoveWriter(logger) + + _, errc := runCommand(ctx, new(BootstrapCommand), "-e", "peckham") + c.Assert(<-errc, gc.IsNil) + c.Assert(testWriter.Log, jc.LogMatches, []string{"ignoring environments.yaml: using bootstrap config in .*"}) +} + func (s *BootstrapSuite) TestInvalidLocalSource(c *gc.C) { s.PatchValue(&version.Current.Number, version.MustParse("1.2.0")) env, fake := makeEmptyFakeHome(c) @@ -349,7 +433,7 @@ Endpoint: "endpoint", } sourceDir := c.MkDir() - sourceStor, err := filestorage.NewFileStorageWriter(sourceDir, filestorage.UseDefaultTmpDir) + sourceStor, err := filestorage.NewFileStorageWriter(sourceDir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, sourceStor) c.Assert(err, gc.IsNil) @@ -446,7 +530,8 @@ s.setupAutoUploadTest(c, "1.8.3", "precise") _, errc := runCommand(nullContext(), new(BootstrapCommand)) err := <-errc - c.Assert(err, gc.ErrorMatches, "cannot find bootstrap tools: no matching tools available") + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, gc.Matches, noToolsAvailableMessage) } func (s *BootstrapSuite) TestMissingToolsError(c *gc.C) { @@ -456,7 +541,7 @@ c.Assert(code, gc.Equals, 1) errText := context.Stderr.(*bytes.Buffer).String() errText = strings.Replace(errText, "\n", "", -1) - expectedErrText := "error: cannot find bootstrap tools: no matching tools available" + expectedErrText := "error: cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*" c.Assert(errText, gc.Matches, expectedErrText) } @@ -472,7 +557,7 @@ c.Assert(code, gc.Equals, 1) errText := context.Stderr.(*bytes.Buffer).String() errText = strings.Replace(errText, "\n", "", -1) - expectedErrText := "error: cannot find bootstrap tools: an error" + expectedErrText := "error: cannot upload bootstrap tools: an error" c.Assert(errText, gc.Matches, expectedErrText) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/cmd_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/cmd_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/cmd_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/cmd_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "os" "reflect" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/dummy" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type CmdSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/common.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/common.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/common.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/common.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,46 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package main + +import ( + "launchpad.net/juju-core/cmd" + "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/configstore" + "launchpad.net/juju-core/errors" +) + +// destroyPreparedEnviron destroys the environment and logs an error if it fails. +func destroyPreparedEnviron(env environs.Environ, store configstore.Storage, err *error, action string) { + if *err == nil { + return + } + if err := environs.Destroy(env, store); err != nil { + logger.Errorf("%s failed, and the environment could not be destroyed: %v", action, err) + } +} + +// environFromName loads an existing environment or prepares a new one. +func environFromName( + ctx *cmd.Context, envName string, resultErr *error, action string) (environs.Environ, func(), error) { + + store, err := configstore.Default() + if err != nil { + return nil, nil, err + } + var existing bool + if environInfo, err := store.ReadInfo(envName); !errors.IsNotFoundError(err) { + existing = true + logger.Warningf("ignoring environments.yaml: using bootstrap config in %s", environInfo.Location()) + } + environ, err := environs.PrepareFromName(envName, ctx, store) + if err != nil { + return nil, nil, err + } + cleanup := func() { + if !existing { + destroyPreparedEnviron(environ, store, resultErr, action) + } + } + return environ, cleanup, nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/constraints_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/constraints_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/constraints_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/constraints_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( "bytes" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/juju/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type ConstraintsCommandsSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/deploy.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/deploy.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy.go 2014-03-20 12:52:38.000000000 +0000 @@ -142,7 +142,7 @@ return err } - repo = config.AuthorizeCharmRepo(repo, conf) + repo = config.SpecializeCharmRepo(repo, conf) curl, err = addCharmViaAPI(client, ctx, curl, repo) if err != nil { @@ -213,7 +213,7 @@ return err } - repo = config.AuthorizeCharmRepo(repo, conf) + repo = config.SpecializeCharmRepo(repo, conf) // TODO(fwereade) it's annoying to roundtrip the bytes through the client // here, but it's the original behaviour and not convenient to change. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/deploy_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/deploy_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -15,7 +16,6 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type DeploySuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go 2014-03-20 12:52:38.000000000 +0000 @@ -67,7 +67,7 @@ } answer := strings.ToLower(scanner.Text()) if answer != "y" && answer != "yes" { - return errors.New("Environment destruction aborted") + return errors.New("environment destruction aborted") } } // If --force is supplied, then don't attempt to use the API. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "bytes" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/dummy" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type destroyEnvSuite struct { @@ -130,7 +130,7 @@ // Ensure confirmation is requested if "-y" is not specified. stdin.WriteString("n") opc, errc := runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") - c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") + c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") c.Check(<-opc, gc.IsNil) c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") assertEnvironNotDestroyed(c, env, s.ConfigStore) @@ -140,7 +140,7 @@ stdout.Reset() opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") c.Check(<-opc, gc.IsNil) - c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") + c.Check(<-errc, gc.ErrorMatches, "environment destruction aborted") assertEnvironNotDestroyed(c, env, s.ConfigStore) // "--yes" passed: no confirmation request. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroymachine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroymachine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/destroymachine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroymachine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,13 +4,13 @@ package main import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type DestroyMachineSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/environment.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/environment.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment.go 2014-03-20 12:52:38.000000000 +0000 @@ -162,26 +162,8 @@ } defer conn.Close() - // Here is the magic around setting the attributes: - // TODO(thumper): get this magic under test somewhere, and update other call-sites to use it. - // Get the existing environment config from the state. - oldConfig, err := conn.State.EnvironConfig() - if err != nil { - return err - } - // Apply the attributes specified for the command to the state config. - newConfig, err := oldConfig.Apply(c.values) - if err != nil { - return err - } - // Now validate this new config against the existing config via the provider. - provider := conn.Environ.Provider() - newProviderConfig, err := provider.Validate(newConfig, oldConfig) - if err != nil { - return err - } - // Now try to apply the new validated config. - return conn.State.SetEnvironConfig(newProviderConfig, oldConfig) + // Update state config with new values + return conn.State.UpdateEnvironConfig(c.values, nil, nil) } func (c *SetEnvironmentCommand) Run(ctx *cmd.Context) error { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/environment_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/environment_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,6 +11,7 @@ jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/dummy" + _ "launchpad.net/juju-core/provider/local" "launchpad.net/juju-core/testing" ) @@ -163,7 +164,7 @@ var immutableConfigTests = map[string]string{ "name": "foo", - "type": "foo", + "type": "local", "firewall-mode": "global", "state-port": "1", "api-port": "666", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/help_topics.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/help_topics.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/help_topics.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/help_topics.go 2014-03-20 12:52:38.000000000 +0000 @@ -24,11 +24,11 @@ juju help topics list all help topics Provider information: - juju help azure use on Windows Azure - juju help ec2 use on Amazon EC2 - juju help hpcloud use on HP Cloud - juju help local use on this computer - juju help openstack use on OpenStack + juju help azure-provider use on Windows Azure + juju help ec2-provider use on Amazon EC2 + juju help hpcloud-provider use on HP Cloud + juju help local-provider use on this computer + juju help openstack-provider use on OpenStack ` const helpProviderStart = ` @@ -47,8 +47,8 @@ ` -const helpLocalProvider = ` -The local provider is a linux-only Juju environment that uses LXC containers as +const helpLocalProvider = ` +The local provider is a Linux-only Juju environment that uses LXC containers as a virtual cloud on the local machine. Because of this, lxc and mongodb are required for the local provider to work. If you don't already have lxc and mongodb installed, run the following commands: @@ -64,14 +64,14 @@ Now you need to tell Juju to use the local provider and then bootstrap: juju switch local - sudo juju bootstrap + juju bootstrap The first time this runs it might take a bit, as it's doing a netinstall for the container, it's around a 300 megabyte download. Subsequent bootstraps -should be much quicker. 'sudo' is needed because only root can create LXC -containers. After the initial bootstrap, you do not need 'sudo' anymore, -except to 'sudo juju destroy-environment' when you want to tear everything -down. +should be much quicker. You'll be asked for your 'sudo' password, which is +needed because only root can create LXC containers. When you need to destroy +the environment, do 'juju destroy-environment local' and you could be asked +for your 'sudo' password again. You deploy charms from the charm store using the following commands: @@ -79,6 +79,24 @@ juju deploy wordpress juju add-relation wordpress mysql +As of trusty, the local provider will prefer to use lxc-clone to create +the machines. A 'template' container is created with the name + juju--tempalte +where is the OS series, for example 'juju-precise-template'. +You can override the use of clone by specifying + use-clone: true +or + use-clone: false +in the configuration for your local provider. If you have the main container +directory mounted on a btrfs partition, then the clone will be using btrfs +snapshots to create the containers. This means that the clones use up much +less disk space. If you do not have btrfs, lxc will attempt to use aufs +(which is an overlay type filesystem). You can explicitly ask Juju to create +full containers and not overlays by specifying the following in the provider +configuration: + use-clone-aufs: false + + References: http://askubuntu.com/questions/65359/how-do-i-configure-juju-for-local-usage @@ -90,31 +108,35 @@ sample_openstack: type: openstack + # Specifies whether the use of a floating IP address is required to # give the nodes a public IP address. Some installations assign public # IP addresses by default without requiring a floating IP address. # use-floating-ip: false + # Specifies whether new machine instances should have the "default" # Openstack security group assigned. # use-default-secgroup: false - admin-secret: 13850d1b9786065cadd0f477e8c97cd3 - # Globally unique swift bucket name - control-bucket: juju-fd6ab8d02393af742bfbe8b9629707ee + # Usually set via the env variable OS_AUTH_URL, but can be specified here # auth-url: https://yourkeystoneurl:443/v2.0/ - # override if your workstation is running a different series to which - # you are deploying + # The following are used for userpass authentication (the default) - auth-mode: userpass + # auth-mode: userpass + # Usually set via the env variable OS_USERNAME, but can be specified here # username: + # Usually set via the env variable OS_PASSWORD, but can be specified here # password: + # Usually set via the env variable OS_TENANT_NAME, but can be specified here # tenant-name: + # Usually set via the env variable OS_REGION_NAME, but can be specified here # region: +If you have set the described OS_* environment variables, you only need "type:". References: http://juju.ubuntu.com/docs/provider-configuration-openstack.html @@ -125,7 +147,7 @@ This answer is for generic OpenStack support, if you're using an OpenStack-based provider check these questions out for provider-specific information: - https://juju.ubuntu.com/docs/config-hpcloud.html + https://juju.ubuntu.com/docs/config-hpcloud.html ` @@ -141,11 +163,8 @@ type: ec2 # access-key: YOUR-ACCESS-KEY-GOES-HERE # secret-key: YOUR-SECRET-KEY-GOES-HERE - control-bucket: juju-faefb490d69a41f0a3616a4808e0766b - admin-secret: 81a1e7429e6847c4941fda7591246594 -See the EC2 provider documentation[2] for more options. The S3 bucket does not -need to exist already. +See the EC2 provider documentation[2] for more options. Note If you already have an AWS account, you can determine your access key by visiting your account page[3], clicking "Security Credentials" and then clicking @@ -183,7 +202,7 @@ See the online help for more information: - https://juju.ubuntu.com/docs/config-hpcloud.html + https://juju.ubuntu.com/docs/config-hpcloud.html ` const helpAzureProvider = ` @@ -191,17 +210,22 @@ sample_azure: type: azure + # Location for instances, e.g. West US, North Europe. location: West US + # http://msdn.microsoft.com/en-us/library/windowsazure # Windows Azure Management info. management-subscription-id: 886413e1-3b8a-5382-9b90-0c9aee199e5d management-certificate-path: /home/me/azure.pem + # Windows Azure Storage info. storage-account-name: juju0useast0 + # Override OS image selection with a fixed image for all deployments. # Most useful for developers. # force-image-name: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-13_10-amd64-server-DEVELOPMENT-20130713-Juju_ALPHA-en-us-30GB + # Pick a simplestreams stream to select OS images from: daily or released # images, or any other stream available on simplestreams. Leave blank for # released images. @@ -219,7 +243,7 @@ https://juju.ubuntu.com/docs/config-azure.html ` -const helpConstraints = ` +const helpConstraints = ` Constraints constrain the possible instances that may be started by juju commands. They are usually passed as a flag to commands that provision a new machine (such as bootstrap, deploy, and add-machine). @@ -272,7 +296,7 @@ T terabytes (1024 gigabytes) P petabytes (1024 terabytes) -root-disk +root-disk Root-Disk is a float that defines the amount of space in megabytes that must be available in the machine's root partition. For providers that have configurable root disk sizes (such as EC2) an instance with the specified @@ -280,8 +304,8 @@ defaults to megabytes and may be specified in the same manner as the mem constraint. -container - Container defines that the machine must be a container of the specified type. +container + Container defines that the machine must be a container of the specified type. A container of that type may be created by juju to fulfill the request. Currently supported containers: none - (default) no container @@ -294,7 +318,7 @@ roughly, a single 2007-era Xeon). Cpu-power is currently only supported by the Amazon EC2 environment. -tags +tags Tags defines the list of tags that the machine must have applied to it. Multiple tags must be delimited by a comma. Tags are currently only supported by the MaaS environment. @@ -374,7 +398,7 @@ other, and the way in which the topology of Services is assembled. The Charm defines which Relations a given Service may establish, and what kind of interface these Relations require. - + In many cases, the establishment of a Relation will result into an actual TCP connection being created between the Service Units, but that's not necessarily the case. Relations may also be established to inform Services of @@ -423,7 +447,7 @@ ` const helpLogging = ` -Juju has logging available for both client and server components. Most +Juju has logging available for both client and server components. Most users' exposure to the logging mechanism is through either the 'debug-log' command, or through the log file stored on the bootstrap node at /var/log/juju/all-machines.log. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/main.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/main.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "fmt" "os" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/environs" @@ -51,16 +51,16 @@ MissingCallback: RunPlugin, }) jujucmd.AddHelpTopic("basics", "Basic commands", helpBasics) - jujucmd.AddHelpTopic("local", "How to configure a local (LXC) provider", + jujucmd.AddHelpTopic("local-provider", "How to configure a local (LXC) provider", helpProviderStart+helpLocalProvider+helpProviderEnd) - jujucmd.AddHelpTopic("openstack", "How to configure an OpenStack provider", - helpProviderStart+helpOpenstackProvider+helpProviderEnd) - jujucmd.AddHelpTopic("ec2", "How to configure an Amazon EC2 provider", - helpProviderStart+helpEC2Provider+helpProviderEnd) - jujucmd.AddHelpTopic("hpcloud", "How to configure an HP Cloud provider", - helpProviderStart+helpHPCloud+helpProviderEnd) - jujucmd.AddHelpTopic("azure", "How to configure a Windows Azure provider", - helpProviderStart+helpAzureProvider+helpProviderEnd) + jujucmd.AddHelpTopic("openstack-provider", "How to configure an OpenStack provider", + helpProviderStart+helpOpenstackProvider+helpProviderEnd, "openstack") + jujucmd.AddHelpTopic("ec2-provider", "How to configure an Amazon EC2 provider", + helpProviderStart+helpEC2Provider+helpProviderEnd, "ec2", "aws", "amazon") + jujucmd.AddHelpTopic("hpcloud-provider", "How to configure an HP Cloud provider", + helpProviderStart+helpHPCloud+helpProviderEnd, "hpcloud", "hp-cloud") + jujucmd.AddHelpTopic("azure-provider", "How to configure a Windows Azure provider", + helpProviderStart+helpAzureProvider+helpProviderEnd, "azure") jujucmd.AddHelpTopic("constraints", "How to use commands with constraints", helpConstraints) jujucmd.AddHelpTopic("glossary", "Glossary of terms", helpGlossary) jujucmd.AddHelpTopic("logging", "How Juju handles logging", helpLogging) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/main_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/main_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -277,17 +277,17 @@ } var topicNames = []string{ - "azure", + "azure-provider", "basics", "commands", "constraints", - "ec2", + "ec2-provider", "global-options", "glossary", - "hpcloud", - "local", + "hpcloud-provider", + "local-provider", "logging", - "openstack", + "openstack-provider", "plugins", "topics", } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/plugin_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/plugin_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/plugin_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/plugin_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,10 +11,10 @@ "text/template" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/run_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/run_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/run_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/run_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,12 +7,12 @@ "fmt" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils/exec" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/synctools.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/synctools.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,15 +4,12 @@ package main import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/juju-core/cmd" - "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/sync" - "launchpad.net/juju-core/errors" "launchpad.net/juju-core/version" ) @@ -84,25 +81,14 @@ // Register writer for output on screen. loggo.RegisterWriter("synctools", cmd.NewCommandLogWriter("juju.environs.sync", ctx.Stdout, ctx.Stderr), loggo.INFO) defer loggo.RemoveWriter("synctools") - store, err := configstore.Default() + environ, cleanup, err := environFromName(ctx, c.EnvName, &resultErr, "Sync-tools") if err != nil { return err } - var existing bool - if _, err := store.ReadInfo(c.EnvName); !errors.IsNotFoundError(err) { - existing = true - } - environ, err := environs.PrepareFromName(c.EnvName, ctx, store) - if err != nil { - return err - } - if !existing { - defer destroyPreparedEnviron(environ, store, &resultErr, "Sync-tools") - } - + defer cleanup() target := environ.Storage() if c.localDir != "" { - target, err = filestorage.NewFileStorageWriter(c.localDir, filestorage.UseDefaultTmpDir) + target, err = filestorage.NewFileStorageWriter(c.localDir) if err != nil { return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/synctools_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/synctools_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,8 @@ "errors" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/environs/sync" "launchpad.net/juju-core/provider/dummy" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradecharm.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradecharm.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradecharm.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradecharm.go 2014-03-20 12:52:38.000000000 +0000 @@ -143,7 +143,7 @@ return err } - repo = config.AuthorizeCharmRepo(repo, conf) + repo = config.SpecializeCharmRepo(repo, conf) // If no explicit revision was set with either SwitchURL // or Revision flags, discover the latest. @@ -215,7 +215,7 @@ return err } - repo = config.AuthorizeCharmRepo(repo, conf) + repo = config.SpecializeCharmRepo(repo, conf) // If no explicit revision was set with either SwitchURL // or Revision flags, discover the latest. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradejuju.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradejuju.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradejuju.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradejuju.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,18 +6,21 @@ import ( stderrors "errors" "fmt" + "os" + "path" "launchpad.net/gnuflag" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/bootstrap" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/environs/sync" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/juju" - "launchpad.net/juju-core/log" + "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" @@ -117,7 +120,7 @@ defer client.Close() defer func() { if err == errUpToDate { - log.Noticef(err.Error()) + logger.Infof(err.Error()) err = nil } }() @@ -134,78 +137,27 @@ if err != nil { return err } - env, err := environs.New(cfg) - if err != nil { - return err - } - v, err := c.initVersions(cfg, env) - if err != nil { - return err - } - if c.UploadTools { - series := getUploadSeries(cfg, c.Series) - if err := v.uploadTools(env.Storage(), series); err != nil { - return err - } - } - if err := v.validate(); err != nil { - return err - } - log.Infof("upgrade version chosen: %s", v.chosen) - // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. - log.Infof("available tools: %s", v.tools) - - if err := client.SetEnvironAgentVersion(v.chosen); err != nil { - return err - } - log.Noticef("started upgrade to %s", v.chosen) - return nil -} - -// run1dot16 implements the command without access to the API. This is -// needed for compatibility, so 1.16 can be upgraded to newer -// releases. It should be removed in 1.18. -func (c *UpgradeJujuCommand) run1dot16() error { - log.Warningf("running in 1.16 compatibility mode") - conn, err := juju.NewConnFromName(c.EnvName) - if err != nil { - return err - } - defer conn.Close() - defer func() { - if err == errUpToDate { - log.Noticef(err.Error()) - err = nil - } - }() - - // Determine the version to upgrade to, uploading tools if necessary. - env := conn.Environ - cfg, err := conn.State.EnvironConfig() - if err != nil { - return err - } - v, err := c.initVersions(cfg, env) + context, err := c.initVersions(client, cfg) if err != nil { return err } if c.UploadTools { - series := getUploadSeries(cfg, c.Series) - if err := v.uploadTools(env.Storage(), series); err != nil { + series := bootstrap.SeriesToUpload(cfg, c.Series) + if err := context.uploadTools(series); err != nil { return err } } - if err := v.validate(); err != nil { + if err := context.validate(); err != nil { return err } - log.Infof("upgrade version chosen: %s", v.chosen) + logger.Infof("upgrade version chosen: %s", context.chosen) // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. - log.Infof("available tools: %s", v.tools) + logger.Infof("available tools: %s", context.tools) - if err := conn.State.SetEnvironAgentVersion(v.chosen); err != nil { + if err := client.SetEnvironAgentVersion(context.chosen); err != nil { return err } - log.Noticef("started upgrade to %s", v.chosen) + logger.Infof("started upgrade to %s", context.chosen) return nil } @@ -213,7 +165,7 @@ // agent and client versions, and the list of currently available tools, will // always be accurate; the chosen version, and the flag indicating development // mode, may remain blank until uploadTools or validate is called. -func (c *UpgradeJujuCommand) initVersions(cfg *config.Config, env environs.Environ) (*upgradeVersions, error) { +func (c *UpgradeJujuCommand) initVersions(client *api.Client, cfg *config.Config) (*upgradeContext, error) { agent, ok := cfg.AgentVersion() if !ok { // Can't happen. In theory. @@ -222,38 +174,60 @@ if c.Version == agent { return nil, errUpToDate } - client := version.Current.Number - // TODO use an API call rather than requiring the environment, - // so that we can restrict access to the provider secrets - // while still allowing users to upgrade. - available, err := envtools.FindTools(env, client.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) + clientVersion := version.Current.Number + findResult, err := client.FindTools(clientVersion.Major, -1, "", "") + var availableTools coretools.List + if params.IsCodeNotImplemented(err) { + availableTools, err = findTools1dot17(cfg) + } else { + availableTools = findResult.List + } if err != nil { - if !errors.IsNotFoundError(err) { + return nil, err + } + err = findResult.Error + if findResult.Error != nil { + if !params.IsCodeNotFound(err) { return nil, err } if !c.UploadTools { - // No tools found and we shouldn't upload any, so pretend - // there is no more recent version available. - if c.Version == version.Zero { + // No tools found and we shouldn't upload any, so if we are not asking for a + // major upgrade, pretend there is no more recent version available. + if c.Version == version.Zero && agent.Major == clientVersion.Major { return nil, errUpToDate } return nil, err } } - return &upgradeVersions{ - agent: agent, - client: client, - chosen: c.Version, - tools: available, + return &upgradeContext{ + agent: agent, + client: clientVersion, + chosen: c.Version, + tools: availableTools, + apiClient: client, + config: cfg, }, nil } -// upgradeVersions holds the version information for making upgrade decisions. -type upgradeVersions struct { - agent version.Number - client version.Number - chosen version.Number - tools coretools.List +// findTools1dot17 allows 1.17.x versions to be upgraded. +func findTools1dot17(cfg *config.Config) (coretools.List, error) { + logger.Warningf("running find tools in 1.17 compatibility mode") + env, err := environs.New(cfg) + if err != nil { + return nil, err + } + clientVersion := version.Current.Number + return envtools.FindTools(env, clientVersion.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) +} + +// upgradeContext holds the version information for making upgrade decisions. +type upgradeContext struct { + agent version.Number + client version.Number + chosen version.Number + tools coretools.List + config *config.Config + apiClient *api.Client } // uploadTools compiles jujud from $GOPATH and uploads it into the supplied @@ -263,7 +237,7 @@ // than that of any otherwise-matching available envtools. // uploadTools resets the chosen version and replaces the available tools // with the ones just uploaded. -func (v *upgradeVersions) uploadTools(storage storage.Storage, series []string) error { +func (context *upgradeContext) uploadTools(series []string) (err error) { // TODO(fwereade): this is kinda crack: we should not assume that // version.Current matches whatever source happens to be built. The // ideal would be: @@ -276,75 +250,104 @@ // ...but there's no way we have time for that now. In the meantime, // considering the use cases, this should work well enough; but it // won't detect an incompatible major-version change, which is a shame. - if v.chosen == version.Zero { - v.chosen = v.client + if context.chosen == version.Zero { + context.chosen = context.client } - v.chosen = uploadVersion(v.chosen, v.tools) + context.chosen = uploadVersion(context.chosen, context.tools) - // TODO(fwereade): envtools.Upload should return envtools.List, and should - // include all the extra series we build, so we can set *that* onto - // v.available and maybe one day be able to check that a given upgrade - // won't leave out-of-date machines lying around, starved of envtools. - uploaded, err := sync.Upload(storage, &v.chosen, series...) + builtTools, err := sync.BuildToolsTarball(&context.chosen) if err != nil { return err } - v.tools = coretools.List{uploaded} + defer os.RemoveAll(builtTools.Dir) + + var uploaded *coretools.Tools + toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) + logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024) + uploaded, err = context.apiClient.UploadTools(toolsPath, builtTools.Version, series...) + if params.IsCodeNotImplemented(err) { + uploaded, err = context.uploadTools1dot17(builtTools, series...) + } + if err != nil { + return err + } + context.tools = coretools.List{uploaded} return nil } +func (context *upgradeContext) uploadTools1dot17(builtTools *sync.BuiltTools, + series ...string) (*coretools.Tools, error) { + + logger.Warningf("running upload tools in 1.17 compatibility mode") + env, err := environs.New(context.config) + if err != nil { + return nil, err + } + return sync.SyncBuiltTools(env.Storage(), builtTools, series...) +} + // validate chooses an upgrade version, if one has not already been chosen, // and ensures the tools list contains no entries that do not have that version. // If validate returns no error, the environment agent-version can be set to // the value of the chosen field. -func (v *upgradeVersions) validate() (err error) { - if v.chosen == version.Zero { - // No explicitly specified version, so find the next available - // stable release to upgrade to, starting from the current agent - // version and doing major.minor+1 or +2 as needed. - nextStable := v.agent - if v.agent.IsDev() { - nextStable.Minor += 1 +func (context *upgradeContext) validate() (err error) { + if context.chosen == version.Zero { + // No explicitly specified version, so find the version to which we + // need to upgrade. If the CLI and agent major versions match, we find + // next available stable release to upgrade to by incrementing the + // minor version, starting from the current agent version and doing + // major.minor+1 or +2 as needed. If the CLI has a greater major version, + // we just use the CLI version as is. + nextVersion := context.agent + if nextVersion.Major == context.client.Major { + if context.agent.IsDev() { + nextVersion.Minor += 1 + } else { + nextVersion.Minor += 2 + } } else { - nextStable.Minor += 2 + nextVersion = context.client } - newestNextStable, found := v.tools.NewestCompatible(nextStable) + newestNextStable, found := context.tools.NewestCompatible(nextVersion) if found { - log.Debugf("found a more recent stable version %s", newestNextStable) - v.chosen = newestNextStable + logger.Debugf("found a more recent stable version %s", newestNextStable) + context.chosen = newestNextStable } else { - newestCurrent, found := v.tools.NewestCompatible(v.agent) + newestCurrent, found := context.tools.NewestCompatible(context.agent) if found { - log.Debugf("found more recent current version %s", newestCurrent) - v.chosen = newestCurrent + logger.Debugf("found more recent current version %s", newestCurrent) + context.chosen = newestCurrent } else { - return fmt.Errorf("no more recent supported versions available") + if context.agent.Major != context.client.Major { + return fmt.Errorf("no compatible tools available") + } else { + return fmt.Errorf("no more recent supported versions available") + } } } } else { // If not completely specified already, pick a single tools version. - filter := coretools.Filter{Number: v.chosen, Released: !v.chosen.IsDev()} - if v.tools, err = v.tools.Match(filter); err != nil { + filter := coretools.Filter{Number: context.chosen, Released: !context.chosen.IsDev()} + if context.tools, err = context.tools.Match(filter); err != nil { return err } - v.chosen, v.tools = v.tools.Newest() + context.chosen, context.tools = context.tools.Newest() } - if v.chosen == v.agent { + if context.chosen == context.agent { return errUpToDate } - // Major version upgrade - if v.chosen.Major < v.agent.Major { + // Disallow major.minor version downgrades. + if context.chosen.Major < context.agent.Major || + context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even // *connecting* to environments with higher agent-versions; but ofc they // have to connect in order to discover they shouldn't. However, once // any of our tools detect an incompatible version, they should act to // minimize damage: the CLI should abort politely, and the agents should // run an Upgrader but no other tasks. - return fmt.Errorf("cannot change major version from %d to %d", v.agent.Major, v.chosen.Major) - } else if v.chosen.Major > v.agent.Major { - return fmt.Errorf("major version upgrades are not supported yet") + return fmt.Errorf("cannot change version from %s to %s", context.agent, context.chosen) } return nil @@ -364,3 +367,94 @@ } return vers } + +// run1dot16 implements the command without access to the API. This is +// needed for compatibility, so 1.16 can be upgraded to newer +// releases. It should be removed in 1.18. +func (c *UpgradeJujuCommand) run1dot16() error { + logger.Warningf("running in 1.16 compatibility mode") + conn, err := juju.NewConnFromName(c.EnvName) + if err != nil { + return err + } + defer conn.Close() + defer func() { + if err == errUpToDate { + logger.Infof(err.Error()) + err = nil + } + }() + + // Determine the version to upgrade to, uploading tools if necessary. + env := conn.Environ + cfg, err := conn.State.EnvironConfig() + if err != nil { + return err + } + context, err := c.initVersions1dot16(cfg, env) + if err != nil { + return err + } + if c.UploadTools { + series := bootstrap.SeriesToUpload(cfg, c.Series) + if err := context.uploadTools1dot16(env.Storage(), series); err != nil { + return err + } + } + if err := context.validate(); err != nil { + return err + } + logger.Infof("upgrade version chosen: %s", context.chosen) + logger.Infof("available tools: %s", context.tools) + + if err := conn.State.SetEnvironAgentVersion(context.chosen); err != nil { + return err + } + logger.Infof("started upgrade to %s", context.chosen) + return nil +} + +func (c *UpgradeJujuCommand) initVersions1dot16(cfg *config.Config, env environs.Environ) (*upgradeContext, error) { + agent, ok := cfg.AgentVersion() + if !ok { + // Can't happen. In theory. + return nil, fmt.Errorf("incomplete environment configuration") + } + if c.Version == agent { + return nil, errUpToDate + } + client := version.Current.Number + available, err := envtools.FindTools(env, client.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) + if err != nil { + if !errors.IsNotFoundError(err) { + return nil, err + } + if !c.UploadTools { + // No tools found and we shouldn't upload any, so if we are not asking for a + // major upgrade, pretend there is no more recent version available. + if c.Version == version.Zero && agent.Major == client.Major { + return nil, errUpToDate + } + return nil, err + } + } + return &upgradeContext{ + agent: agent, + client: client, + chosen: c.Version, + tools: available, + }, nil +} + +func (context *upgradeContext) uploadTools1dot16(storage storage.Storage, series []string) error { + if context.chosen == version.Zero { + context.chosen = context.client + } + context.chosen = uploadVersion(context.chosen, context.tools) + uploaded, err := sync.Upload(storage, &context.chosen, series...) + if err != nil { + return err + } + context.tools = coretools.List{uploaded} + return nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradejuju_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradejuju_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/juju/upgradejuju_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/upgradejuju_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,6 +11,7 @@ "io/ioutil" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/filestorage" @@ -20,13 +21,12 @@ envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/juju/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" - coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) type UpgradeJujuSuite struct { testing.JujuConnSuite + toolsDir string } var _ = gc.Suite(&UpgradeJujuSuite{}) @@ -90,7 +90,7 @@ }, { about: "--upload-tools with inappropriate version 2", currentVersion: "3.2.7-quantal-amd64", - args: []string{"--upload-tools", "--version", "3.1.0.4"}, + args: []string{"--upload-tools", "--version", "3.2.8.4"}, expectInitErr: "cannot specify build number when uploading tools", }, { about: "latest supported stable release", @@ -105,6 +105,24 @@ agentVersion: "2.0.0", expectVersion: "2.0.5", }, { + about: "latest current release matching CLI, major version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectVersion: "3.2.0", +}, { + about: "latest current release matching CLI, major version, no matching major tools", + tools: []string{"2.8.2-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectErr: "no matching tools available", +}, { + about: "latest current release matching CLI, major version, no matching tools", + tools: []string{"3.3.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + expectErr: "no compatible tools available", +}, { about: "no next supported available", tools: []string{"2.1.0-quantal-amd64", "2.1.5-quantal-i386", "2.3.3-quantal-amd64"}, currentVersion: "2.0.0-quantal-amd64", @@ -130,6 +148,13 @@ args: []string{"--version", "2.3.0"}, expectVersion: "2.3.0", }, { + about: "specified major version", + tools: []string{"3.2.0-quantal-amd64"}, + currentVersion: "3.2.0-quantal-amd64", + agentVersion: "2.8.2", + args: []string{"--version", "3.2.0"}, + expectVersion: "3.2.0", +}, { about: "specified version missing, but already set", currentVersion: "3.0.0-quantal-amd64", agentVersion: "3.0.0", @@ -140,7 +165,7 @@ currentVersion: "3.0.0-quantal-amd64", agentVersion: "3.0.0", args: []string{"--version", "3.2.0"}, - expectErr: "no matching tools available", + expectErr: "no tools available", }, { about: "specified version, no matching major version", tools: []string{"4.2.0-quantal-amd64"}, @@ -175,14 +200,14 @@ currentVersion: "3.2.0-quantal-amd64", agentVersion: "4.2.0", args: []string{"--version", "3.2.0"}, - expectErr: "cannot change major version from 4 to 3", + expectErr: "cannot change version from 4.2.0 to 3.2.0", }, { - about: "major version upgrade to compatible version", + about: "minor version downgrade to incompatible version", tools: []string{"3.2.0-quantal-amd64"}, currentVersion: "3.2.0-quantal-amd64", - agentVersion: "2.8.2", + agentVersion: "3.3.0", args: []string{"--version", "3.2.0"}, - expectErr: "major version upgrades are not supported yet", + expectErr: "cannot change version from 3.3.0 to 3.2.0", }, { about: "nothing available", currentVersion: "2.0.0-quantal-amd64", @@ -248,32 +273,33 @@ expectUploaded: []string{"2.7.3.2-quantal-amd64", "2.7.3.2-precise-amd64", "2.7.3.2-raring-amd64"}, }} -// mockUploadTools simulates the effect of tools.Upload, but skips the time- -// consuming build from source. -// TODO(fwereade) better factor agent/tools such that build logic is -// exposed and can itself be neatly mocked? -func mockUploadTools(stor storage.Storage, forceVersion *version.Number, series ...string) (*coretools.Tools, error) { - vers := version.Current - if forceVersion != nil { - vers.Number = *forceVersion - } - versions := []version.Binary{vers} - for _, series := range series { - if series != version.Current.Series { - newVers := vers - newVers.Series = series - versions = append(versions, newVers) +// getMockBuildTools returns a sync.BuildToolsTarballFunc implementation which generates +// a fake tools tarball. +func (s *UpgradeJujuSuite) getMockBuildTools(c *gc.C) sync.BuildToolsTarballFunc { + return func(forceVersion *version.Number) (*sync.BuiltTools, error) { + // UploadFakeToolsVersions requires a storage to write to. + stor, err := filestorage.NewFileStorageWriter(s.toolsDir) + c.Assert(err, gc.IsNil) + vers := version.Current + if forceVersion != nil { + vers.Number = *forceVersion } + versions := []version.Binary{vers} + uploadedTools, err := envtesting.UploadFakeToolsVersions(stor, versions...) + c.Assert(err, gc.IsNil) + agentTools := uploadedTools[0] + return &sync.BuiltTools{ + Dir: s.toolsDir, + StorageName: envtools.StorageName(vers), + Version: vers, + Size: agentTools.Size, + Sha256Hash: agentTools.SHA256, + }, nil } - agentTools, err := envtesting.UploadFakeToolsVersions(stor, versions...) - if err != nil { - return nil, err - } - return agentTools[0], nil } func (s *UpgradeJujuSuite) TestUpgradeJuju(c *gc.C) { - s.PatchValue(&sync.Upload, mockUploadTools) + s.PatchValue(&sync.BuildToolsTarball, s.getMockBuildTools(c)) oldVersion := version.Current defer func() { version.Current = oldVersion @@ -296,25 +322,24 @@ } // Set up state and environ, and run the command. - oldcfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) toolsDir := c.MkDir() - cfg, err := oldcfg.Apply(map[string]interface{}{ + updateAttrs := map[string]interface{}{ "agent-version": test.agentVersion, "tools-metadata-url": "file://" + toolsDir, - }) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) + } + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) c.Assert(err, gc.IsNil) versions := make([]version.Binary, len(test.tools)) for i, v := range test.tools { versions[i] = version.MustParseBinary(v) - } - envtesting.MustUploadFakeToolsVersions(s.Conn.Environ.Storage(), versions...) - stor, err := filestorage.NewFileStorageWriter(toolsDir, "") - c.Assert(err, gc.IsNil) - envtesting.MustUploadFakeToolsVersions(stor, versions...) + if len(versions) > 0 { + envtesting.MustUploadFakeToolsVersions(s.Conn.Environ.Storage(), versions...) + stor, err := filestorage.NewFileStorageWriter(toolsDir) + c.Assert(err, gc.IsNil) + envtesting.MustUploadFakeToolsVersions(stor, versions...) + } + err = com.Run(coretesting.Context(c)) if test.expectErr != "" { c.Check(err, gc.ErrorMatches, test.expectErr) @@ -324,7 +349,7 @@ } // Check expected changes to environ/state. - cfg, err = s.State.EnvironConfig() + cfg, err := s.State.EnvironConfig() c.Check(err, gc.IsNil) agentVersion, ok := cfg.AgentVersion() c.Check(ok, gc.Equals, true) @@ -339,7 +364,9 @@ data, err := ioutil.ReadAll(r) r.Close() c.Check(err, gc.IsNil) - checkToolsContent(c, data, "jujud contents "+uploaded) + expectContent := version.Current + expectContent.Number = agentVersion + checkToolsContent(c, data, "jujud contents "+expectContent.String()) } } } @@ -377,15 +404,13 @@ func (s *UpgradeJujuSuite) Reset(c *gc.C) { s.JujuConnSuite.Reset(c) envtesting.RemoveTools(c, s.Conn.Environ.Storage()) - oldcfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - cfg, err := oldcfg.Apply(map[string]interface{}{ + updateAttrs := map[string]interface{}{ "default-series": "raring", "agent-version": "1.2.3", - }) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) + } + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) c.Assert(err, gc.IsNil) + s.toolsDir = c.MkDir() } func (s *UpgradeJujuSuite) TestUpgradeJujuWithRealUpload(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/agent.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/agent.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/agent.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/agent.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,6 +14,7 @@ "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/log" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" apiagent "launchpad.net/juju-core/state/api/agent" @@ -40,6 +41,10 @@ // addFlags injects common agent flags into f. func (c *AgentConf) addFlags(f *gnuflag.FlagSet) { + // TODO(dimitern) 2014-02-19 bug 1282025 + // We need to pass a config location here instead and + // use it to locate the conf and the infer the data-dir + // from there instead of passing it like that. f.StringVar(&c.dataDir, "data-dir", "/var/lib/juju", "directory for juju data") } @@ -51,7 +56,7 @@ } func (c *AgentConf) read(tag string) (err error) { - c.config, err = agent.ReadConf(c.dataDir, tag) + c.config, err = agent.ReadConf(agent.ConfigPath(c.dataDir, tag)) return } @@ -211,7 +216,7 @@ if ug, ok := err.(*upgrader.UpgradeReadyError); ok { if err := ug.ChangeAgentTools(); err != nil { // Return and let upstart deal with the restart. - return err + return log.LoggedErrorf(logger, "cannot change agent tools: %v", err) } } return err diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/agent_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/agent_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/agent_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/agent_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "fmt" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -21,7 +22,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" @@ -271,7 +271,7 @@ } func (s *agentSuite) testOpenAPIState(c *gc.C, ent state.AgentEntity, agentCmd Agent, initialPassword string) { - conf, err := agent.ReadConf(s.DataDir(), ent.Tag()) + conf, err := agent.ReadConf(agent.ConfigPath(s.DataDir(), ent.Tag())) c.Assert(err, gc.IsNil) // Check that it starts initially and changes the password @@ -326,7 +326,7 @@ } func (s *agentSuite) assertCanOpenState(c *gc.C, tag, dataDir string) { - config, err := agent.ReadConf(dataDir, tag) + config, err := agent.ReadConf(agent.ConfigPath(dataDir, tag)) c.Assert(err, gc.IsNil) st, err := config.OpenState(environs.NewStatePolicy()) c.Assert(err, gc.IsNil) @@ -334,7 +334,7 @@ } func (s *agentSuite) assertCannotOpenState(c *gc.C, tag, dataDir string) { - config, err := agent.ReadConf(dataDir, tag) + config, err := agent.ReadConf(agent.ConfigPath(dataDir, tag)) c.Assert(err, gc.IsNil) _, err = config.OpenState(environs.NewStatePolicy()) expectErr := fmt.Sprintf("cannot log in to juju database as %q: unauthorized mongo access: auth fails", tag) @@ -342,7 +342,7 @@ } func refreshConfig(c *gc.C, config agent.Config) agent.Config { - config, err := agent.ReadConf(config.DataDir(), config.Tag()) + config, err := agent.ReadConf(agent.ConfigPath(config.DataDir(), config.Tag())) c.Assert(err, gc.IsNil) return config } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 @@ -21,6 +21,7 @@ "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" ) // Cloud-init write the URL to be used to load the bootstrap state into this file. @@ -75,17 +76,14 @@ if err != nil { return err } - // agent.BootstrapJobs is an optional field in the agent - // config, and was introduced after 1.17.2. We default to - // allowing units on machine-0 if missing. - jobs := []state.MachineJob{ - state.JobManageEnviron, - state.JobHostUnits, - } - if bootstrapJobs := c.Conf.config.Value(agent.BootstrapJobs); bootstrapJobs != "" { - jobs, err = agent.UnmarshalBootstrapJobs(bootstrapJobs) - if err != nil { - return err + // agent.Jobs is an optional field in the agent config, and was + // introduced after 1.17.2. We default to allowing units on + // machine-0 if missing. + jobs := c.Conf.config.Jobs() + if len(jobs) == 0 { + jobs = []params.MachineJob{ + params.JobManageEnviron, + params.JobHostUnits, } } var characteristics instance.HardwareCharacteristics diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "io/ioutil" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -20,8 +21,8 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -33,6 +34,7 @@ testbase.LoggingSuite testing.MgoSuite dataDir string + logDir string providerStateURLFile string } @@ -68,6 +70,7 @@ s.LoggingSuite.SetUpTest(c) s.MgoSuite.SetUpTest(c) s.dataDir = c.MkDir() + s.logDir = c.MkDir() } func (s *BootstrapSuite) TearDownTest(c *gc.C) { @@ -81,12 +84,20 @@ return utils.UserPasswordHash(testPassword, utils.CompatSalt) } -func (s *BootstrapSuite) initBootstrapCommand(c *gc.C, args ...string) (machineConf agent.Config, cmd *BootstrapCommand, err error) { +func (s *BootstrapSuite) initBootstrapCommand(c *gc.C, jobs []params.MachineJob, args ...string) (machineConf agent.Config, cmd *BootstrapCommand, err error) { ioutil.WriteFile(s.providerStateURLFile, []byte("test://localhost/provider-state\n"), 0600) + if len(jobs) == 0 { + // Add default jobs. + jobs = []params.MachineJob{ + params.JobManageEnviron, params.JobHostUnits, + } + } // NOTE: the old test used an equivalent of the NewAgentConfig, but it // really should be using NewStateMachineConfig. params := agent.AgentConfigParams{ + LogDir: s.logDir, DataDir: s.dataDir, + Jobs: jobs, Tag: "bootstrap", UpgradedToVersion: version.Current.Number, Password: testPasswordHash(), @@ -112,7 +123,7 @@ } func (s *BootstrapSuite) TestInitializeEnvironment(c *gc.C) { - _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig) + _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -139,7 +150,7 @@ func (s *BootstrapSuite) TestSetConstraints(c *gc.C) { tcons := constraints.Value{Mem: uint64p(2048), CpuCores: uint64p(2)} - _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig, "--constraints", tcons.String()) + _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig, "--constraints", tcons.String()) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -168,7 +179,10 @@ } func (s *BootstrapSuite) TestDefaultMachineJobs(c *gc.C) { - _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig) + expectedJobs := []state.MachineJob{ + state.JobManageEnviron, state.JobHostUnits, + } + _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -182,18 +196,12 @@ defer st.Close() m, err := st.Machine("0") c.Assert(err, gc.IsNil) - c.Assert(m.Jobs(), gc.DeepEquals, []state.MachineJob{ - state.JobManageEnviron, state.JobHostUnits, - }) + c.Assert(m.Jobs(), gc.DeepEquals, expectedJobs) } func (s *BootstrapSuite) TestConfiguredMachineJobs(c *gc.C) { - agentConf, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig) - c.Assert(err, gc.IsNil) - bootstrapJobs, err := agent.MarshalBootstrapJobs(state.JobManageEnviron) - c.Assert(err, gc.IsNil) - agentConf.SetValue(agent.BootstrapJobs, bootstrapJobs) - err = agentConf.Write() + jobs := []params.MachineJob{params.JobManageEnviron} + _, cmd, err := s.initBootstrapCommand(c, jobs, "--env-config", testConfig) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -223,7 +231,7 @@ } func (s *BootstrapSuite) TestInitialPassword(c *gc.C) { - machineConf, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig) + machineConf, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) c.Assert(err, gc.IsNil) err = cmd.Run(nil) @@ -254,7 +262,7 @@ // Check that the machine configuration has been given a new // password and that we can connect to mongo as that machine // and that the in-mongo password also verifies correctly. - machineConf1, err := agent.ReadConf(machineConf.DataDir(), "machine-0") + machineConf1, err := agent.ReadConf(agent.ConfigPath(machineConf.DataDir(), "machine-0")) c.Assert(err, gc.IsNil) st, err = machineConf1.OpenState(environs.NewStatePolicy()) @@ -294,7 +302,7 @@ c.Logf("test %d", i) var args []string args = append(args, t.input...) - _, cmd, err := s.initBootstrapCommand(c, args...) + _, cmd, err := s.initBootstrapCommand(c, nil, args...) if t.err == "" { c.Assert(cmd, gc.NotNil) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/machine.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/machine.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/machine.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/machine.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,9 +7,10 @@ "fmt" "os" "path/filepath" + "runtime" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/tomb" @@ -28,6 +29,7 @@ "launchpad.net/juju-core/state/apiserver" "launchpad.net/juju-core/upgrades" "launchpad.net/juju-core/upstart" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker" "launchpad.net/juju-core/worker/authenticationworker" @@ -60,6 +62,8 @@ var jujuRun = "/usr/local/bin/juju-run" +var useMultipleCPUs = utils.UseMultipleCPUs + // MachineAgent is a cmd.Command responsible for running a machine agent. type MachineAgent struct { cmd.CommandBase @@ -68,6 +72,9 @@ MachineId string runner worker.Runner upgradeComplete chan struct{} + stateOpened chan struct{} + workersStarted chan struct{} + st *state.State } // Info returns usage information for the command. @@ -93,6 +100,8 @@ } a.runner = newRunner(isFatal, moreImportant) a.upgradeComplete = make(chan struct{}) + a.stateOpened = make(chan struct{}) + a.workersStarted = make(chan struct{}) return nil } @@ -115,7 +124,7 @@ // lines of all logging in the log file. loggo.RemoveWriter("logfile") defer a.tomb.Done() - logger.Infof("machine agent %v start (%s)", a.Tag(), version.Current) + logger.Infof("machine agent %v start (%s [%s])", a.Tag(), version.Current, runtime.Compiler) if err := a.Conf.read(a.Tag()); err != nil { return err } @@ -152,6 +161,8 @@ a.runner.StartWorker("termination", func() (worker.Worker, error) { return terminationworker.NewWorker(), nil }) + // At this point, all workers will have been configured to start + close(a.workersStarted) err := a.runner.Wait() if err == worker.ErrTerminateAgent { err = a.uninstallAgent() @@ -310,6 +321,8 @@ if err != nil { return nil, err } + a.st = st + close(a.stateOpened) reportOpenedState(st) m := entity.(*state.Machine) @@ -330,6 +343,7 @@ case state.JobHostUnits: // Implemented in APIWorker. case state.JobManageEnviron: + useMultipleCPUs() a.startWorkerAfterUpgrade(runner, "instancepoller", func() (worker.Worker, error) { return instancepoller.NewWorker(st), nil }) @@ -413,11 +427,22 @@ return nil default: } - err := a.runUpgrades(apiState, jobs) + // If the machine agent is a state server, wait until state is opened. + var st *state.State + for _, job := range jobs { + if job == params.JobManageEnviron { + select { + case <-a.stateOpened: + } + st = a.st + break + } + } + err := a.runUpgrades(st, apiState, jobs) if err != nil { return err } - logger.Infof("Upgrade to %v completed.", version.Current) + logger.Infof("upgrade to %v completed.", version.Current) close(a.upgradeComplete) <-stop return nil @@ -425,15 +450,15 @@ } // runUpgrades runs the upgrade operations for each job type and updates the updatedToVersion on success. -func (a *MachineAgent) runUpgrades(st *api.State, jobs []params.MachineJob) error { +func (a *MachineAgent) runUpgrades(st *state.State, apiState *api.State, jobs []params.MachineJob) error { agentConfig := a.Conf.config from := version.Current from.Number = agentConfig.UpgradedToVersion() if from == version.Current { - logger.Infof("Upgrade to %v already completed.", version.Current) + logger.Infof("upgrade to %v already completed.", version.Current) return nil } - context := upgrades.NewContext(agentConfig, st) + context := upgrades.NewContext(agentConfig, apiState, st) for _, job := range jobs { var target upgrades.Target switch job { @@ -444,14 +469,21 @@ default: continue } - logger.Infof("Starting upgrade from %v to %v for %v", from, version.Current, target) + logger.Infof("starting upgrade from %v to %v for %v %q", from, version.Current, target, a.Tag()) if err := upgrades.PerformUpgrade(from.Number, target, context); err != nil { - return fmt.Errorf("cannot perform upgrade from %v to %v for %v: %v", from, version.Current, target, err) + return fmt.Errorf("cannot perform upgrade from %v to %v for %v %q: %v", from, version.Current, target, a.Tag(), err) } } return a.Conf.config.WriteUpgradedToVersion(version.Current.Number) } +// WorkersStarted returns a channel that's closed once all top level workers +// have been started. This is provided for testing purposes. +func (a *MachineAgent) WorkersStarted() <-chan struct{} { + return a.workersStarted + +} + func (a *MachineAgent) Entity(st *state.State) (AgentState, error) { m, err := st.Machine(a.MachineId) if err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/machine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/machine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/machine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/machine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,8 @@ "strings" "time" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -31,12 +33,8 @@ "launchpad.net/juju-core/state/api/params" apirsyslog "launchpad.net/juju-core/state/api/rsyslog" charmtesting "launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing" - statetesting "launchpad.net/juju-core/state/testing" "launchpad.net/juju-core/state/watcher" - "launchpad.net/juju-core/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" - "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/ssh" @@ -59,8 +57,9 @@ func (s *commonMachineSuite) SetUpSuite(c *gc.C) { s.agentSuite.SetUpSuite(c) s.TestSuite.SetUpSuite(c) - restore := testbase.PatchValue(&charm.CacheDir, c.MkDir()) + restore := testing.PatchValue(&charm.CacheDir, c.MkDir()) s.AddSuiteCleanup(func(*gc.C) { restore() }) + } func (s *commonMachineSuite) TearDownSuite(c *gc.C) { @@ -357,7 +356,7 @@ } func (s *MachineSuite) TestManageEnvironRunsInstancePoller(c *gc.C) { - defer testbase.PatchValue(&instancepoller.ShortPoll, 500*time.Millisecond).Restore() + s.PatchValue(&instancepoller.ShortPoll, 500*time.Millisecond) usefulVersion := version.Current usefulVersion.Series = "quantal" // to match the charm created below envtesting.AssertUploadFakeToolsVersions(c, s.Conn.Environ.Storage(), usefulVersion) @@ -398,6 +397,48 @@ } +func (s *MachineSuite) TestManageEnvironCallsUseMultipleCPUs(c *gc.C) { + // If it has been enabled, the JobManageEnviron agent should call utils.UseMultipleCPUs + usefulVersion := version.Current + usefulVersion.Series = "quantal" + envtesting.AssertUploadFakeToolsVersions(c, s.Conn.Environ.Storage(), usefulVersion) + m, _, _ := s.primeAgent(c, version.Current, state.JobManageEnviron) + s.setFakeMachineAddresses(c, m) + calledChan := make(chan struct{}, 1) + s.PatchValue(&useMultipleCPUs, func() { calledChan <- struct{}{} }) + // Now, start the agent, and observe that a JobManageEnviron agent + // calls UseMultipleCPUs + a := s.newAgent(c, m) + defer a.Stop() + go func() { + c.Check(a.Run(nil), gc.IsNil) + }() + // Wait for configuration to be finished + <-a.WorkersStarted() + select { + case <-calledChan: + case <-time.After(coretesting.LongWait): + c.Errorf("we failed to call UseMultipleCPUs()") + } + c.Check(a.Stop(), gc.IsNil) + // However, an agent that just JobHostUnits doesn't call UseMultipleCPUs + m2, _, _ := s.primeAgent(c, version.Current, state.JobHostUnits) + s.setFakeMachineAddresses(c, m2) + a2 := s.newAgent(c, m2) + defer a2.Stop() + go func() { + c.Check(a2.Run(nil), gc.IsNil) + }() + // Wait until all the workers have been started, and then kill everything + <-a2.workersStarted + c.Check(a2.Stop(), gc.IsNil) + select { + case <-calledChan: + c.Errorf("we should not have called UseMultipleCPUs()") + case <-time.After(coretesting.ShortWait): + } +} + func (s *MachineSuite) waitProvisioned(c *gc.C, unit *state.Unit) (*state.Machine, instance.Id) { c.Logf("waiting for unit %q to be provisioned", unit) machineId, err := unit.AssignedMachineId() @@ -635,7 +676,7 @@ // Update the keys in the environment. sshKey := sshtesting.ValidKeyOne.Key + " user@host" - err := statetesting.UpdateConfig(s.BackingState, map[string]interface{}{"authorized-keys": sshKey}) + err := s.BackingState.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": sshKey}, nil, nil) c.Assert(err, gc.IsNil) // Wait for ssh keys file to be updated. @@ -723,25 +764,21 @@ s.primeAgent(c, version.Current, state.JobHostUnits) // Make sure there are some proxy settings to write. - oldConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - proxySettings := osenv.ProxySettings{ Http: "http proxy", Https: "https proxy", Ftp: "ftp proxy", } - envConfig, err := oldConfig.Apply(config.ProxyConfigMap(proxySettings)) - c.Assert(err, gc.IsNil) + updateAttrs := config.ProxyConfigMap(proxySettings) - err = s.State.SetEnvironConfig(envConfig, oldConfig) + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) c.Assert(err, gc.IsNil) s.assertJobWithAPI(c, state.JobHostUnits, func(conf agent.Config, st *api.State) { for { select { - case <-time.After(testing.LongWait): + case <-time.After(coretesting.LongWait): c.Fatalf("timeout while waiting for proxy settings to change") case <-time.After(10 * time.Millisecond): _, err := os.Stat(utils.AptConfFile) @@ -786,7 +823,7 @@ }) s.assertJobWithAPI(c, job, func(conf agent.Config, st *api.State) { select { - case <-time.After(testing.LongWait): + case <-time.After(coretesting.LongWait): c.Fatalf("timeout while waiting for rsyslog worker to be created") case mode := <-created: c.Assert(mode, gc.Equals, expectedMode) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/main.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/main.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main.go 2014-03-20 12:52:38.000000000 +0000 @@ -1,4 +1,4 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package main @@ -12,7 +12,7 @@ "strings" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/utils/exec" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/main_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/main_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,13 +14,13 @@ "strings" stdtesting "testing" + "github.com/juju/testing" "launchpad.net/gnuflag" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/testing" - "launchpad.net/juju-core/testing/testbase" + coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/worker/deployer" "launchpad.net/juju-core/worker/uniter/jujuc" ) @@ -52,19 +52,22 @@ // Change the default init dir in worker/deployer, // so the deployer doesn't try to remove upstart // jobs from tests. - restore := testbase.PatchValue(&deployer.InitDir, mkdtemp("juju-worker-deployer")) + restore := testing.PatchValue(&deployer.InitDir, mkdtemp("juju-worker-deployer")) defer restore() + // TODO(waigani) 2014-03-19 bug 1294458 + // Refactor to use base suites + // Change the path to "juju-run", so that the // tests don't try to write to /usr/local/bin. jujuRun = mktemp("juju-run", "") defer os.Remove(jujuRun) // Create a CA certificate available for all tests. - caCertFile = mktemp("juju-test-cert", testing.CACert) + caCertFile = mktemp("juju-test-cert", coretesting.CACert) defer os.Remove(caCertFile) - testing.MgoTestPackage(t) + coretesting.MgoTestPackage(t) } type MainSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/run.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/run.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/run.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/run.go 2014-03-20 12:52:38.000000000 +0000 @@ -119,7 +119,7 @@ socketPath := filepath.Join(unitDir, uniter.RunListenerFile) // make sure the socket exists - client, err := rpc.Dial(uniter.RunListenerNetType, socketPath) + client, err := rpc.Dial("unix", socketPath) if err != nil { return nil, err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/run_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/run_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/run_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/run_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,12 +9,12 @@ "path/filepath" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/exec" "launchpad.net/juju-core/utils/fslock" @@ -181,7 +181,7 @@ c.Assert(err, gc.IsNil) socketPath := filepath.Join(testAgentDir, uniter.RunListenerFile) - listener, err := uniter.NewRunListener(&mockRunner{c}, "unix", socketPath) + listener, err := uniter.NewRunListener(&mockRunner{c}, socketPath) c.Assert(err, gc.IsNil) c.Assert(listener, gc.NotNil) s.AddCleanup(func(*gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/unit.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/unit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/unit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/unit.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,8 +5,9 @@ import ( "fmt" + "runtime" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/tomb" @@ -72,7 +73,7 @@ if err := a.Conf.read(a.Tag()); err != nil { return err } - agentLogger.Infof("unit agent %v start (%s)", a.Tag(), version.Current) + agentLogger.Infof("unit agent %v start (%s [%s])", a.Tag(), version.Current, runtime.Compiler) a.runner.StartWorker("api", a.APIWorkers) err := agentDone(a.runner.Wait()) a.tomb.Kill(err) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/unit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/unit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/unit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/unit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,17 +4,12 @@ package main import ( - "encoding/json" - "io/ioutil" - "os" - "path/filepath" "time" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" "launchpad.net/juju-core/cmd" - "launchpad.net/juju-core/environs" envtesting "launchpad.net/juju-core/environs/testing" jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/names" @@ -150,27 +145,10 @@ agent := s.newAgent(c, unit) newVers := version.Current newVers.Patch++ - envtesting.PrimeTools(c, s.Conn.Environ.Storage(), s.DataDir(), newVers) - - // Set up fake downloaded tools for the assigned machine. - libDir := c.MkDir() - s.PatchValue(&environs.DataDir, libDir) - fakeToolsPath := filepath.Join(libDir, "tools", newVers.String()) - err := os.MkdirAll(fakeToolsPath, 0700) - c.Assert(err, gc.IsNil) - fakeTools := tools.Tools{ - Version: newVers, - URL: "fake-url", - Size: 1234, - SHA256: "checksum", - } - toolsMetadataData, err := json.Marshal(fakeTools) - c.Assert(err, gc.IsNil) - err = ioutil.WriteFile(filepath.Join(fakeToolsPath, "downloaded-tools.txt"), []byte(toolsMetadataData), 0644) - c.Assert(err, gc.IsNil) + envtesting.AssertUploadFakeToolsVersions(c, s.Conn.Environ.Storage(), newVers) // Set the machine agent version to trigger an upgrade. - err = machine.SetAgentVersion(newVers) + err := machine.SetAgentVersion(newVers) c.Assert(err, gc.IsNil) err = runWithTimeout(agent) envtesting.CheckUpgraderReadyError(c, err, &upgrader.UpgradeReadyError{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/upgrade_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/upgrade_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/jujud/upgrade_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/upgrade_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,20 +5,24 @@ import ( "os" + "os/exec" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/state" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) type UpgradeSuite struct { commonMachineSuite + aptCmds []*exec.Cmd machine *state.Machine upgradeToVersion version.Binary } @@ -30,6 +34,15 @@ func (s *UpgradeSuite) SetUpTest(c *gc.C) { s.commonMachineSuite.SetUpTest(c) + // Capture all apt commands. + s.aptCmds = nil + aptCmds := s.HookCommandOutput(&utils.AptCommandOutput, nil, nil) + go func() { + for cmd := range aptCmds { + s.aptCmds = append(s.aptCmds, cmd) + } + }() + // As Juju versions increase, update the version to which we are upgrading. s.upgradeToVersion = version.Current s.upgradeToVersion.Number.Minor++ @@ -69,7 +82,7 @@ // Wait for upgrade steps to run. success := false for attempt := coretesting.LongAttempt.Start(); attempt.Next(); { - conf, err := agent.ReadConf(oldConfig.DataDir(), s.machine.Tag()) + conf, err := agent.ReadConf(agent.ConfigPath(oldConfig.DataDir(), s.machine.Tag())) c.Assert(err, gc.IsNil) success = conf.UpgradedToVersion() == s.upgradeToVersion.Number if success { @@ -84,17 +97,42 @@ return filepath.Join(s.DataDir(), "system-identity") } +func (s *UpgradeSuite) assertCommonUpgrades(c *gc.C) { + // rsyslog-gnutls should have been installed. + c.Assert(s.aptCmds, gc.HasLen, 1) + args := s.aptCmds[0].Args + c.Assert(len(args), jc.GreaterThan, 1) + c.Assert(args[0], gc.Equals, "apt-get") + c.Assert(args[len(args)-1], gc.Equals, "rsyslog-gnutls") +} + func (s *UpgradeSuite) assertStateServerUpgrades(c *gc.C) { + s.assertCommonUpgrades(c) // System SSH key c.Assert(s.keyFile(), jc.IsNonEmptyFile) + // Syslog port should have been updated + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + c.Assert(cfg.SyslogPort(), gc.Equals, config.DefaultSyslogPort) + // Deprecated attributes should have been deleted - just test a couple. + allAttrs := cfg.AllAttrs() + _, ok := allAttrs["public-bucket"] + c.Assert(ok, jc.IsFalse) + _, ok = allAttrs["public-bucket-region"] + c.Assert(ok, jc.IsFalse) } func (s *UpgradeSuite) assertHostUpgrades(c *gc.C) { + s.assertCommonUpgrades(c) // Lock directory lockdir := filepath.Join(s.DataDir(), "locks") c.Assert(lockdir, jc.IsDirectory) // SSH key file should not be generated for hosts. _, err := os.Stat(s.keyFile()) c.Assert(err, jc.Satisfies, os.IsNotExist) + // Syslog port should not have been updated + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + c.Assert(cfg.SyslogPort(), gc.Not(gc.Equals), config.DefaultSyslogPort) // Add other checks as needed... } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/logging.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/logging.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,7 +9,7 @@ "os" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/juju-core/juju/osenv" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/logging_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/logging_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,19 +7,19 @@ "io/ioutil" "path/filepath" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "github.com/juju/testing" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/juju/osenv" - "launchpad.net/juju-core/testing" - "launchpad.net/juju-core/testing/testbase" + coretesting "launchpad.net/juju-core/testing" ) var logger = loggo.GetLogger("juju.test") type LogSuite struct { - testbase.CleanupSuite + testing.CleanupSuite } var _ = gc.Suite(&LogSuite{}) @@ -34,7 +34,7 @@ func newLogWithFlags(c *gc.C, flags []string) *cmd.Log { log := &cmd.Log{} - flagSet := testing.NewFlagSet() + flagSet := coretesting.NewFlagSet() log.AddFlags(flagSet) err := flagSet.Parse(false, flags) c.Assert(err, gc.IsNil) @@ -60,7 +60,7 @@ func (s *LogSuite) TestLogConfigFromEnvironment(c *gc.C) { config := "juju.cmd=INFO;juju.worker.deployer=DEBUG" - testbase.PatchEnvironment(osenv.JujuLoggingConfigEnvKey, config) + s.PatchEnvironment(osenv.JujuLoggingConfigEnvKey, config) log := newLogWithFlags(c, []string{}) c.Assert(log.Path, gc.Equals, "") c.Assert(log.Verbose, gc.Equals, false) @@ -70,67 +70,67 @@ func (s *LogSuite) TestVerboseSetsLogLevel(c *gc.C) { l := &cmd.Log{Verbose: true} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) c.Assert(loggo.GetLogger("").LogLevel(), gc.Equals, loggo.INFO) - c.Assert(testing.Stderr(ctx), gc.Equals, "") - c.Assert(testing.Stdout(ctx), gc.Equals, "Flag --verbose is deprecated with the current meaning, use --show-log\n") + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "Flag --verbose is deprecated with the current meaning, use --show-log\n") } func (s *LogSuite) TestDebugSetsLogLevel(c *gc.C) { l := &cmd.Log{Debug: true} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) c.Assert(loggo.GetLogger("").LogLevel(), gc.Equals, loggo.DEBUG) - c.Assert(testing.Stderr(ctx), gc.Equals, "") - c.Assert(testing.Stdout(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } func (s *LogSuite) TestShowLogSetsLogLevel(c *gc.C) { l := &cmd.Log{ShowLog: true} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) c.Assert(loggo.GetLogger("").LogLevel(), gc.Equals, loggo.INFO) - c.Assert(testing.Stderr(ctx), gc.Equals, "") - c.Assert(testing.Stdout(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } func (s *LogSuite) TestStderr(c *gc.C) { l := &cmd.Log{Verbose: true, Config: "=INFO"} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) logger.Infof("hello") - c.Assert(testing.Stderr(ctx), gc.Matches, `^.* INFO .* hello\n`) + c.Assert(coretesting.Stderr(ctx), gc.Matches, `^.* INFO .* hello\n`) } func (s *LogSuite) TestRelPathLog(c *gc.C) { l := &cmd.Log{Path: "foo.log", Config: "=INFO"} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) logger.Infof("hello") content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "foo.log")) c.Assert(err, gc.IsNil) c.Assert(string(content), gc.Matches, `^.* INFO .* hello\n`) - c.Assert(testing.Stderr(ctx), gc.Equals, "") - c.Assert(testing.Stdout(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } func (s *LogSuite) TestAbsPathLog(c *gc.C) { path := filepath.Join(c.MkDir(), "foo.log") l := &cmd.Log{Path: path, Config: "=INFO"} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) logger.Infof("hello") - c.Assert(testing.Stderr(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") content, err := ioutil.ReadFile(path) c.Assert(err, gc.IsNil) c.Assert(string(content), gc.Matches, `^.* INFO .* hello\n`) @@ -138,26 +138,26 @@ func (s *LogSuite) TestLoggingToFileAndStderr(c *gc.C) { l := &cmd.Log{Path: "foo.log", Config: "=INFO", ShowLog: true} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) logger.Infof("hello") content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "foo.log")) c.Assert(err, gc.IsNil) c.Assert(string(content), gc.Matches, `^.* INFO .* hello\n`) - c.Assert(testing.Stderr(ctx), gc.Matches, `^.* INFO .* hello\n`) - c.Assert(testing.Stdout(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Matches, `^.* INFO .* hello\n`) + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } func (s *LogSuite) TestErrorAndWarningLoggingToStderr(c *gc.C) { // Error and warning go to stderr even with ShowLog=false l := &cmd.Log{Config: "=INFO", ShowLog: false} - ctx := testing.Context(c) + ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) logger.Warningf("a warning") logger.Errorf("an error") logger.Infof("an info") - c.Assert(testing.Stderr(ctx), gc.Matches, `^.*WARNING a warning\n.*ERROR an error\n.*`) - c.Assert(testing.Stdout(ctx), gc.Equals, "") + c.Assert(coretesting.Stderr(ctx), gc.Matches, `^.*WARNING a warning\n.*ERROR an error\n.*`) + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/package_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -23,5 +23,5 @@ // really be moved into "juju/osenv". c.Assert(testbase.FindJujuCoreImports(c, "launchpad.net/juju-core/cmd"), gc.DeepEquals, - []string{"juju/osenv", "names", "version"}) + []string{"juju/arch", "juju/osenv", "names", "version"}) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -18,6 +18,7 @@ "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" + "launchpad.net/juju-core/juju/arch" ) // ImageMetadataCommand is used to write out simplestreams image metadata information. @@ -53,7 +54,7 @@ func (c *ImageMetadataCommand) SetFlags(f *gnuflag.FlagSet) { c.EnvCommandBase.SetFlags(f) f.StringVar(&c.Series, "s", "", "the charm series") - f.StringVar(&c.Arch, "a", "amd64", "the image achitecture") + f.StringVar(&c.Arch, "a", arch.AMD64, "the image achitecture") f.StringVar(&c.Dir, "d", "", "the destination directory in which to place the metadata files") f.StringVar(&c.ImageId, "i", "", "the image id") f.StringVar(&c.Region, "r", "", "the region") @@ -160,7 +161,7 @@ Region: c.Region, Endpoint: c.Endpoint, } - targetStorage, err := filestorage.NewFileStorageWriter(c.Dir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(c.Dir) if err != nil { return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/imagemetadata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,11 +11,11 @@ "path/filepath" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "fmt" "os" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/juju" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "path/filepath" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/juju-core/cmd" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/signmetadata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "path/filepath" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,11 +5,8 @@ import ( "fmt" - "net/url" - "path" - "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/juju-core/cmd" @@ -60,19 +57,12 @@ const minorVersion = -1 toolsList, err := envtools.ReadList(sourceStorage, version.Current.Major, minorVersion) if err == envtools.ErrNoTools { - source := envtools.DefaultBaseURL - var u *url.URL - u, err = url.Parse(source) + var source string + source, err = envtools.ToolsURL(envtools.DefaultBaseURL) if err != nil { - return fmt.Errorf("invalid tools source %s: %v", source, err) + return err } - if u.Scheme == "" { - source = "file://" + source - if !strings.HasSuffix(source, "/"+storage.BaseToolsPath) { - source = path.Join(source, storage.BaseToolsPath) - } - } - sourceDataSource := simplestreams.NewURLDataSource(source, simplestreams.VerifySSLHostnames) + sourceDataSource := simplestreams.NewURLDataSource("local source", source, simplestreams.VerifySSLHostnames) toolsList, err = envtools.FindToolsForCloud( []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, version.Current.Major, minorVersion, coretools.Filter{}) @@ -81,7 +71,7 @@ return err } - targetStorage, err := filestorage.NewFileStorageWriter(c.metadataDir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(c.metadataDir) if err != nil { return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "regexp" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -22,6 +22,7 @@ // ValidateImageMetadataCommand type ValidateImageMetadataCommand struct { cmd.EnvCommandBase + out cmd.Output providerType string metadataDir string series string @@ -79,6 +80,7 @@ func (c *ValidateImageMetadataCommand) SetFlags(f *gnuflag.FlagSet) { c.EnvCommandBase.SetFlags(f) + c.out.AddFlags(f, "smart", cmd.DefaultFormatters) f.StringVar(&c.providerType, "p", "", "the provider type eg ec2, openstack") f.StringVar(&c.metadataDir, "d", "", "directory where metadata files are found") f.StringVar(&c.series, "s", "", "the series for which to validate (overrides env config series)") @@ -180,26 +182,43 @@ if _, err := os.Stat(dir); err != nil { return err } - params.Sources = []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+dir, simplestreams.VerifySSLHostnames)} + params.Sources = []simplestreams.DataSource{ + simplestreams.NewURLDataSource( + "local metadata directory", "file://"+dir, simplestreams.VerifySSLHostnames), + } } params.Stream = c.stream - image_ids, err := imagemetadata.ValidateImageMetadata(params) + image_ids, resolveInfo, err := imagemetadata.ValidateImageMetadata(params) if err != nil { + if resolveInfo != nil { + metadata := map[string]interface{}{ + "Resolve Metadata": *resolveInfo, + } + if metadataYaml, yamlErr := cmd.FormatYaml(metadata); yamlErr == nil { + err = fmt.Errorf("%v\n%v", err, string(metadataYaml)) + } + } return err } - if len(image_ids) > 0 { - fmt.Fprintf(context.Stdout, "matching image ids for region %q:\n%s\n", params.Region, strings.Join(image_ids, "\n")) + metadata := map[string]interface{}{ + "ImageIds": image_ids, + "Region": params.Region, + "Resolve Metadata": *resolveInfo, + } + c.out.Write(context, metadata) } else { - var urls []string + var sources []string for _, s := range params.Sources { url, err := s.URL("") - if err != nil { - urls = append(urls, url) + if err == nil { + sources = append(sources, fmt.Sprintf("- %s (%s)", s.Description(), url)) } } - return fmt.Errorf("no matching image ids for region %s using URLs:\n%s", params.Region, strings.Join(urls, "\n")) + return fmt.Errorf( + "no matching image ids for region %s using sources:\n%s", + params.Region, strings.Join(sources, "\n")) } return nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -75,7 +75,7 @@ Region: region, Endpoint: endpoint, } - targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir) if err != nil { return err } @@ -133,7 +133,9 @@ c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching image ids for region "us-east-1":.*`) + c.Check( + strippedOut, gc.Matches, + `ImageIds:.*"1234".*Region:.*us-east-1.*Resolve Metadata:.*source: local metadata directory.*`) } func (s *ValidateImageMetadataSuite) TestEc2LocalMetadataUsingEnvironment(c *gc.C) { @@ -143,10 +145,10 @@ } func (s *ValidateImageMetadataSuite) TestEc2LocalMetadataUsingIncompleteEnvironment(c *gc.C) { - testbase.PatchEnvironment("AWS_ACCESS_KEY_ID", "") - testbase.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "") - testbase.PatchEnvironment("EC2_ACCESS_KEY", "") - testbase.PatchEnvironment("EC2_SECRET_KEY", "") + s.PatchEnvironment("AWS_ACCESS_KEY_ID", "") + s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "") + s.PatchEnvironment("EC2_ACCESS_KEY", "") + s.PatchEnvironment("EC2_SECRET_KEY", "") s.setupEc2LocalMetadata(c, "us-east-1", "") ctx := coretesting.Context(c) code := cmd.Main( @@ -169,7 +171,9 @@ c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching image ids for region "us-west-1":.*`) + c.Check( + strippedOut, gc.Matches, + `ImageIds:.*"1234".*Region:.*us-west-1.*Resolve Metadata:.*source: local metadata directory.*`) } func (s *ValidateImageMetadataSuite) TestEc2LocalMetadataNoMatch(c *gc.C) { @@ -187,6 +191,9 @@ "-u", "https://ec2.region.amazonaws.com", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) + errOut := ctx.Stderr.(*bytes.Buffer).String() + strippedOut := strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, `.*Resolve Metadata:.*`) } func (s *ValidateImageMetadataSuite) TestOpenstackLocalMetadataWithManualParams(c *gc.C) { @@ -200,7 +207,9 @@ c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching image ids for region "region-2":.*`) + c.Check( + strippedOut, gc.Matches, + `ImageIds:.*"1234".*Region:.*region-2.*Resolve Metadata:.*source: local metadata directory.*`) } func (s *ValidateImageMetadataSuite) TestOpenstackLocalMetadataNoMatch(c *gc.C) { @@ -212,10 +221,16 @@ "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) + errOut := ctx.Stderr.(*bytes.Buffer).String() + strippedOut := strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, `.*Resolve Metadata:.*`) code = cmd.Main( &ValidateImageMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-3", "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) + errOut = ctx.Stderr.(*bytes.Buffer).String() + strippedOut = strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, `.*Resolve Metadata:.*`) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,12 +15,14 @@ "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/tools" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/version" ) // ValidateToolsMetadataCommand type ValidateToolsMetadataCommand struct { cmd.EnvCommandBase + out cmd.Output providerType string metadataDir string series string @@ -100,6 +102,7 @@ func (c *ValidateToolsMetadataCommand) SetFlags(f *gnuflag.FlagSet) { c.EnvCommandBase.SetFlags(f) + c.out.AddFlags(f, "smart", cmd.DefaultFormatters) f.StringVar(&c.providerType, "p", "", "the provider type eg ec2, openstack") f.StringVar(&c.metadataDir, "d", "", "directory where metadata files are found") f.StringVar(&c.series, "s", "", "the series for which to validate (overrides env config series)") @@ -160,7 +163,7 @@ return err } params = &simplestreams.MetadataLookupParams{ - Architectures: []string{"amd64", "arm", "i386"}, + Architectures: arch.AllSupportedArches, } } } else { @@ -191,30 +194,48 @@ if _, err := os.Stat(c.metadataDir); err != nil { return err } - params.Sources = []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+c.metadataDir, simplestreams.VerifySSLHostnames)} + toolsURL, err := tools.ToolsURL(c.metadataDir) + if err != nil { + return err + } + params.Sources = []simplestreams.DataSource{simplestreams.NewURLDataSource( + "local metadata directory", toolsURL, simplestreams.VerifySSLHostnames), + } } - versions, err := tools.ValidateToolsMetadata(&tools.ToolsMetadataLookupParams{ + versions, resolveInfo, err := tools.ValidateToolsMetadata(&tools.ToolsMetadataLookupParams{ MetadataLookupParams: *params, Version: c.exactVersion, Major: c.major, Minor: c.minor, }) if err != nil { + if resolveInfo != nil { + metadata := map[string]interface{}{ + "Resolve Metadata": *resolveInfo, + } + if metadataYaml, yamlErr := cmd.FormatYaml(metadata); yamlErr == nil { + err = fmt.Errorf("%v\n%v", err, string(metadataYaml)) + } + } return err } if len(versions) > 0 { - fmt.Fprintf(context.Stdout, "matching tools versions:\n%s\n", strings.Join(versions, "\n")) + metadata := map[string]interface{}{ + "Matching Tools Versions": versions, + "Resolve Metadata": *resolveInfo, + } + c.out.Write(context, metadata) } else { - var urls []string + var sources []string for _, s := range params.Sources { url, err := s.URL("") - if err != nil { - urls = append(urls, url) + if err == nil { + sources = append(sources, fmt.Sprintf("- %s (%s)", s.Description(), url)) } } - return fmt.Errorf("no matching tools using URLs:\n%s", strings.Join(urls, "\n")) + return fmt.Errorf("no matching tools using sources:\n%s", strings.Join(sources, "\n")) } return nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,9 +11,8 @@ gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" - "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/tools" - "launchpad.net/juju-core/juju/osenv" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" @@ -21,7 +20,8 @@ type ValidateToolsMetadataSuite struct { testbase.LoggingSuite - home *coretesting.FakeHome + home *coretesting.FakeHome + metadataDir string } var _ = gc.Suite(&ValidateToolsMetadataSuite{}) @@ -72,16 +72,14 @@ } func (s *ValidateToolsMetadataSuite) makeLocalMetadata(c *gc.C, version, region, series, endpoint string) error { - tm := tools.ToolsMetadata{ + tm := []*tools.ToolsMetadata{{ Version: version, Arch: "amd64", Release: series, - } - cloudSpec := simplestreams.CloudSpec{ - Region: region, - Endpoint: endpoint, - } - _, err := tools.MakeBoilerplate(&tm, &cloudSpec, false) + }} + targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir) + c.Assert(err, gc.IsNil) + err = tools.WriteMetadata(targetStorage, tm, false) if err != nil { return err } @@ -91,10 +89,9 @@ func (s *ValidateToolsMetadataSuite) SetUpTest(c *gc.C) { s.LoggingSuite.SetUpTest(c) s.home = coretesting.MakeFakeHome(c, metadataTestEnvConfig) - restore := testbase.PatchEnvironment("AWS_ACCESS_KEY_ID", "access") - s.AddCleanup(func(*gc.C) { restore() }) - restore = testbase.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "secret") - s.AddCleanup(func(*gc.C) { restore() }) + s.metadataDir = c.MkDir() + s.PatchEnvironment("AWS_ACCESS_KEY_ID", "access") + s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "secret") } func (s *ValidateToolsMetadataSuite) TearDownTest(c *gc.C) { @@ -114,19 +111,18 @@ func (s *ValidateToolsMetadataSuite) TestEc2LocalMetadataUsingEnvironment(c *gc.C) { s.setupEc2LocalMetadata(c, "us-east-1") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( - &ValidateToolsMetadataCommand{}, ctx, []string{"-e", "ec2", "-j", "1.11.4", "-d", metadataDir}, + &ValidateToolsMetadataCommand{}, ctx, []string{"-e", "ec2", "-j", "1.11.4", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestEc2LocalMetadataUsingIncompleteEnvironment(c *gc.C) { - testbase.PatchEnvironment("AWS_ACCESS_KEY_ID", "") - testbase.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "") + s.PatchEnvironment("AWS_ACCESS_KEY_ID", "") + s.PatchEnvironment("AWS_SECRET_ACCESS_KEY", "") s.setupEc2LocalMetadata(c, "us-east-1") ctx := coretesting.Context(c) code := cmd.Main( @@ -141,123 +137,121 @@ func (s *ValidateToolsMetadataSuite) TestEc2LocalMetadataWithManualParams(c *gc.C) { s.setupEc2LocalMetadata(c, "us-west-1") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "ec2", "-s", "precise", "-r", "us-west-1", "-j", "1.11.4", - "-u", "https://ec2.us-west-1.amazonaws.com", "-d", metadataDir}, + "-u", "https://ec2.us-west-1.amazonaws.com", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestEc2LocalMetadataNoMatch(c *gc.C) { s.setupEc2LocalMetadata(c, "us-east-1") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "ec2", "-s", "raring", "-r", "us-west-1", - "-u", "https://ec2.us-west-1.amazonaws.com", "-d", metadataDir}, + "-u", "https://ec2.us-west-1.amazonaws.com", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) code = cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "ec2", "-s", "precise", "-r", "region", - "-u", "https://ec2.region.amazonaws.com", "-d", metadataDir}, + "-u", "https://ec2.region.amazonaws.com", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) + errOut := ctx.Stderr.(*bytes.Buffer).String() + strippedOut := strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, `.*Resolve Metadata:.*`) } func (s *ValidateToolsMetadataSuite) TestOpenstackLocalMetadataWithManualParams(c *gc.C) { s.makeLocalMetadata(c, "1.11.4", "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-2", "-j", "1.11.4", - "-u", "some-auth-url", "-d", metadataDir}, + "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestOpenstackLocalMetadataNoMatch(c *gc.C) { s.makeLocalMetadata(c, "1.11.4", "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "precise", "-r", "region-2", - "-u", "some-auth-url", "-d", metadataDir}, + "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) code = cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-3", - "-u", "some-auth-url", "-d", metadataDir}, + "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 1) + errOut := ctx.Stderr.(*bytes.Buffer).String() + strippedOut := strings.Replace(errOut, "\n", "", -1) + c.Check(strippedOut, gc.Matches, `.*Resolve Metadata:.*`) } func (s *ValidateToolsMetadataSuite) TestDefaultVersion(c *gc.C) { s.makeLocalMetadata(c, version.Current.Number.String(), "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-2", - "-u", "some-auth-url", "-d", metadataDir}, + "-u", "some-auth-url", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestMajorVersionMatch(c *gc.C) { s.makeLocalMetadata(c, "1.11.4", "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-2", - "-u", "some-auth-url", "-d", metadataDir, "-m", "1"}, + "-u", "some-auth-url", "-d", s.metadataDir, "-m", "1"}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestMajorMinorVersionMatch(c *gc.C) { s.makeLocalMetadata(c, "1.12.1", "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( &ValidateToolsMetadataCommand{}, ctx, []string{ "-p", "openstack", "-s", "raring", "-r", "region-2", - "-u", "some-auth-url", "-d", metadataDir, "-m", "1.12"}, + "-u", "some-auth-url", "-d", s.metadataDir, "-m", "1.12"}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } func (s *ValidateToolsMetadataSuite) TestJustDirectory(c *gc.C) { s.makeLocalMetadata(c, version.Current.Number.String(), "region-2", "raring", "some-auth-url") ctx := coretesting.Context(c) - metadataDir := osenv.JujuHomePath("") code := cmd.Main( - &ValidateToolsMetadataCommand{}, ctx, []string{"-s", "raring", "-d", metadataDir}, + &ValidateToolsMetadataCommand{}, ctx, []string{"-s", "raring", "-d", s.metadataDir}, ) c.Assert(code, gc.Equals, 0) errOut := ctx.Stdout.(*bytes.Buffer).String() strippedOut := strings.Replace(errOut, "\n", "", -1) - c.Check(strippedOut, gc.Matches, `matching tools versions:.*`) + c.Check(strippedOut, gc.Matches, `Matching Tools Versions:.*Resolve Metadata.*`) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "path" "text/template" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/goyaml" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,13 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local + +var ( + // Variable is the address so we can PatchValue + CheckIfRoot = &checkIfRoot + + // function exports for tests + RunAsRoot = runAsRoot + JujuLocalPlugin = jujuLocalPlugin +) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/juju-local/main.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/juju-local/main.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/juju-local/main.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/juju-local/main.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,17 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package main + +import ( + "os" + + "launchpad.net/juju-core/cmd/plugins/local" + + // Import only the local provider. + _ "launchpad.net/juju-core/provider/local" +) + +func main() { + local.Main(os.Args) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/main.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/main.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,75 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local + +import ( + "os" + "os/exec" + + "github.com/juju/loggo" + + "launchpad.net/juju-core/cmd" +) + +var logger = loggo.GetLogger("juju.plugins.local") + +const localDoc = ` + +Juju local is used to provide extra commands that assist with the local +provider. + +See Also: + juju help local-provider +` + +func jujuLocalPlugin() cmd.Command { + plugin := cmd.NewSuperCommand(cmd.SuperCommandParams{ + Name: "juju local", + UsagePrefix: "juju", + Doc: localDoc, + Purpose: "local provider specific commands", + Log: &cmd.Log{}, + }) + + return plugin +} + +// Main registers subcommands for the juju-local executable. +func Main(args []string) { + plugin := jujuLocalPlugin() + os.Exit(cmd.Main(plugin, cmd.DefaultContext(), args[1:])) +} + +var checkIfRoot = func() bool { + return os.Getuid() == 0 +} + +// runAsRoot ensures that the executable is running as root. +// If checkIfRoot returns true, the call function is called, +// otherwise executable is executed using sudo and the extra args +// passed through. +func runAsRoot(executable string, args []string, context *cmd.Context, call func(*cmd.Context) error) error { + if checkIfRoot() { + logger.Debugf("running as root") + return call(context) + } + + logger.Debugf("running as user") + + fullpath, err := exec.LookPath(executable) + if err != nil { + return err + } + + sudoArgs := []string{"--preserve-env", fullpath} + sudoArgs = append(sudoArgs, args...) + + command := exec.Command("sudo", sudoArgs...) + // Now hook up stdin, stdout, stderr + command.Stdin = context.Stdin + command.Stdout = context.Stdout + command.Stderr = context.Stderr + // And run it! + return command.Run() +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/main_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/main_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,78 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local_test + +import ( + "fmt" + "os/exec" + "strings" + + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/cmd" + "launchpad.net/juju-core/cmd/plugins/local" + coretesting "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/testing/testbase" +) + +type mainSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&mainSuite{}) + +func (*mainSuite) TestRegisteredCommands(c *gc.C) { + expectedSubcommands := []string{ + "help", + // TODO: add some as they get registered + } + plugin := local.JujuLocalPlugin() + ctx, err := coretesting.RunCommand(c, plugin, []string{"help", "commands"}) + c.Assert(err, gc.IsNil) + + lines := strings.Split(coretesting.Stdout(ctx), "\n") + var names []string + for _, line := range lines { + f := strings.Fields(line) + if len(f) == 0 { + continue + } + names = append(names, f[0]) + } + // The names should be output in alphabetical order, so don't sort. + c.Assert(names, gc.DeepEquals, expectedSubcommands) +} + +func (s *mainSuite) TestRunAsRootCallsFuncIfRoot(c *gc.C) { + s.PatchValue(local.CheckIfRoot, func() bool { return true }) + called := false + call := func(*cmd.Context) error { + called = true + return nil + } + args := []string{"ignored..."} + err := local.RunAsRoot("juju-magic", args, coretesting.Context(c), call) + c.Assert(err, gc.IsNil) + c.Assert(called, jc.IsTrue) +} + +func (s *mainSuite) TestRunAsRootCallsSudoIfNotRoot(c *gc.C) { + s.PatchValue(local.CheckIfRoot, func() bool { return false }) + testing.PatchExecutableAsEchoArgs(c, s, "sudo") + // the command needs to be in the path... + testing.PatchExecutableAsEchoArgs(c, s, "juju-magic") + magicPath, err := exec.LookPath("juju-magic") + c.Assert(err, gc.IsNil) + callIgnored := func(*cmd.Context) error { + panic("unreachable") + } + args := []string{"passed"} + context := coretesting.Context(c) + err = local.RunAsRoot("juju-magic", args, context, callIgnored) + c.Assert(err, gc.IsNil) + expected := fmt.Sprintf("sudo \"--preserve-env\" %q \"passed\"\n", magicPath) + c.Assert(coretesting.Stdout(context), gc.Equals, expected) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/plugins/local/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local_test + +import ( + testing "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/supercommand.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/supercommand.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/supercommand.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/supercommand.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,11 +7,14 @@ "bytes" "fmt" "io/ioutil" + "runtime" "sort" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" + + "launchpad.net/juju-core/version" ) var logger = loggo.GetLogger("juju.cmd") @@ -19,6 +22,9 @@ type topic struct { short string long func() string + // Help aliases are not output when topics are listed, but are used + // to search for the help topic + alias bool } type UnrecognizedCommand struct { @@ -103,8 +109,8 @@ // param, and the full text being the long param. The description is shown in // 'help topics', and the full text is shown when the command 'help ' is // called. -func (c *SuperCommand) AddHelpTopic(name, short, long string) { - c.subcmds["help"].(*helpCommand).addTopic(name, short, echo(long)) +func (c *SuperCommand) AddHelpTopic(name, short, long string, aliases ...string) { + c.subcmds["help"].(*helpCommand).addTopic(name, short, echo(long), aliases...) } // AddHelpTopicCallback adds a new help topic with the description being the @@ -288,6 +294,7 @@ return err } } + logger.Infof("running juju-%s [%s]", version.Current, runtime.Compiler) err := c.subcmd.Run(ctx) if err != nil && err != ErrSilent { logger.Errorf("%v", err) @@ -353,11 +360,17 @@ return func() string { return s } } -func (c *helpCommand) addTopic(name, short string, long func() string) { +func (c *helpCommand) addTopic(name, short string, long func() string, aliases ...string) { if _, found := c.topics[name]; found { panic(fmt.Sprintf("help topic already added: %s", name)) } - c.topics[name] = topic{short, long} + c.topics[name] = topic{short, long, false} + for _, alias := range aliases { + if _, found := c.topics[alias]; found { + panic(fmt.Sprintf("help topic already added: %s", alias)) + } + c.topics[alias] = topic{short, long, true} + } } func (c *helpCommand) globalOptions() string { @@ -377,15 +390,16 @@ } func (c *helpCommand) topicList() string { - topics := make([]string, len(c.topics)) - i := 0 + var topics []string longest := 0 - for name := range c.topics { + for name, topic := range c.topics { + if topic.alias { + continue + } if len(name) > longest { longest = len(name) } - topics[i] = name - i++ + topics = append(topics, name) } sort.Strings(topics) for i, name := range topics { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/cmd/supercommand_test.go juju-core-1.17.6/src/launchpad.net/juju-core/cmd/supercommand_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/cmd/supercommand_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/cmd/supercommand_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -178,7 +178,8 @@ ctx := testing.Context(c) code := cmd.Main(jc, ctx, []string{"blah", "--option", "error", "--debug"}) c.Assert(code, gc.Equals, 1) - c.Assert(bufferString(ctx.Stderr), gc.Matches, `^.* ERROR .* BAM! + c.Assert(bufferString(ctx.Stderr), gc.Matches, `^.* running juju-.* +.* ERROR .* BAM! `) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/constraints/constraints.go juju-core-1.17.6/src/launchpad.net/juju-core/constraints/constraints.go --- juju-core-1.17.4/src/launchpad.net/juju-core/constraints/constraints.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/constraints/constraints.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,6 +12,7 @@ "github.com/errgo/errgo" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" ) // Value describes a user's requirements of the hardware on which units @@ -279,10 +280,7 @@ if v.Arch != nil { return fmt.Errorf("already set") } - switch str { - case "": - case "amd64", "i386", "arm": - default: + if str != "" && !arch.IsSupportedArch(str) { return fmt.Errorf("%q not recognized", str) } v.Arch = &str diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/constraints/constraints_test.go juju-core-1.17.6/src/launchpad.net/juju-core/constraints/constraints_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/constraints/constraints_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/constraints/constraints_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,12 +7,12 @@ "encoding/json" "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/instance" - jc "launchpad.net/juju-core/testing/checkers" ) func TestPackage(t *testing.T) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/directory_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/directory_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/directory_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/directory_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,10 +7,10 @@ "os" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/container" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/factory/factory.go juju-core-1.17.6/src/launchpad.net/juju-core/container/factory/factory.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/factory/factory.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/factory/factory.go 2014-03-20 12:52:38.000000000 +0000 @@ -19,7 +19,7 @@ func NewContainerManager(forType instance.ContainerType, conf container.ManagerConfig) (container.Manager, error) { switch forType { case instance.LXC: - return lxc.NewContainerManager(conf), nil + return lxc.NewContainerManager(conf) case instance.KVM: return kvm.NewContainerManager(conf) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/factory/factory_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/factory/factory_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/factory/factory_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/factory/factory_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -19,8 +19,6 @@ var _ = gc.Suite(&factorySuite{}) func (*factorySuite) TestNewContainerManager(c *gc.C) { - conf := container.ManagerConfig{Name: "test"} - for _, test := range []struct { containerType instance.ContainerType valid bool @@ -37,6 +35,7 @@ containerType: instance.ContainerType("other"), valid: false, }} { + conf := container.ManagerConfig{container.ConfigName: "test"} manager, err := factory.NewContainerManager(test.containerType, conf) if test.valid { c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/interface.go juju-core-1.17.6/src/launchpad.net/juju-core/container/interface.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/interface.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/interface.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,23 +8,25 @@ "launchpad.net/juju-core/instance" ) +const ( + ConfigName = "name" + ConfigLogDir = "log-dir" +) + // ManagerConfig contains the initialization parameters for the ContainerManager. // The name of the manager is used to namespace the containers on the machine. -type ManagerConfig struct { - Name string - LogDir string -} +type ManagerConfig map[string]string // Manager is responsible for starting containers, and stopping and listing // containers that it has started. type Manager interface { - // StartContainer creates and starts a new container for the specified machine. - StartContainer( + // CreateContainer creates and starts a new container for the specified machine. + CreateContainer( machineConfig *cloudinit.MachineConfig, series string, network *NetworkConfig) (instance.Instance, *instance.HardwareCharacteristics, error) - // StopContainer stops and destroyes the container identified by Instance. - StopContainer(instance.Instance) error + // DestroyContainer stops and destroyes the container identified by Instance. + DestroyContainer(instance.Instance) error // ListContainers return a list of containers that have been started by // this manager. ListContainers() ([]instance.Instance, error) @@ -37,3 +39,19 @@ // that the host machine can run containers. Initialise() error } + +// PopValue returns the requested key from the config map. If the value +// doesn't exist, the function returns the empty string. If the value does +// exist, the value is returned, and the element removed from the map. +func (m ManagerConfig) PopValue(key string) string { + value := m[key] + delete(m, key) + return value +} + +// WarnAboutUnused emits a warning about each value in the map. +func (m ManagerConfig) WarnAboutUnused() { + for key, value := range m { + logger.Warningf("unused config option: %q -> %q", key, value) + } +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/kvm.go juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/kvm.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/kvm.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/kvm.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,8 +8,9 @@ "os/exec" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/container" "launchpad.net/juju-core/environs/cloudinit" @@ -55,13 +56,16 @@ // containers. The containers that are created are namespaced by the name // parameter. func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) { - if conf.Name == "" { + name := conf.PopValue(container.ConfigName) + if name == "" { return nil, fmt.Errorf("name is required") } - if conf.LogDir == "" { - conf.LogDir = "/var/log/juju" + logDir := conf.PopValue(container.ConfigLogDir) + if logDir == "" { + logDir = agent.DefaultLogDir } - return &containerManager{name: conf.Name, logdir: conf.LogDir}, nil + conf.WarnAboutUnused() + return &containerManager{name: name, logdir: logDir}, nil } // containerManager handles all of the business logic at the juju specific @@ -74,7 +78,7 @@ var _ container.Manager = (*containerManager)(nil) -func (manager *containerManager) StartContainer( +func (manager *containerManager) CreateContainer( machineConfig *cloudinit.MachineConfig, series string, network *container.NetworkConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { @@ -121,7 +125,7 @@ return &kvmInstance{kvmContainer, name}, &hardware, nil } -func (manager *containerManager) StopContainer(instance instance.Instance) error { +func (manager *containerManager) DestroyContainer(instance instance.Instance) error { name := string(instance.Id()) kvmContainer := KvmObjectFactory.New(name) if err := kvmContainer.Stop(); err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/kvm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/kvm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/kvm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/kvm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,8 @@ import ( "path/filepath" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -15,7 +16,6 @@ kvmtesting "launchpad.net/juju-core/container/kvm/testing" containertesting "launchpad.net/juju-core/container/testing" "launchpad.net/juju-core/instance" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -30,16 +30,25 @@ func (s *KVMSuite) SetUpTest(c *gc.C) { s.TestSuite.SetUpTest(c) var err error - s.manager, err = kvm.NewContainerManager(container.ManagerConfig{Name: "test"}) + s.manager, err = kvm.NewContainerManager(container.ManagerConfig{container.ConfigName: "test"}) c.Assert(err, gc.IsNil) } func (*KVMSuite) TestManagerNameNeeded(c *gc.C) { - manager, err := kvm.NewContainerManager(container.ManagerConfig{}) + manager, err := kvm.NewContainerManager(container.ManagerConfig{container.ConfigName: ""}) c.Assert(err, gc.ErrorMatches, "name is required") c.Assert(manager, gc.IsNil) } +func (*KVMSuite) TestManagerWarnsAboutUnknownOption(c *gc.C) { + _, err := kvm.NewContainerManager(container.ManagerConfig{ + container.ConfigName: "BillyBatson", + "shazam": "Captain Marvel", + }) + c.Assert(err, gc.IsNil) + c.Assert(c.GetTestLog(), jc.Contains, `WARNING juju.container unused config option: "shazam" -> "Captain Marvel"`) +} + func (s *KVMSuite) TestListInitiallyEmpty(c *gc.C) { containers, err := s.manager.ListContainers() c.Assert(err, gc.IsNil) @@ -79,17 +88,17 @@ c.Assert(string(containers[0].Id()), gc.Equals, running.Name()) } -func (s *KVMSuite) TestStartContainer(c *gc.C) { - instance := containertesting.StartContainer(c, s.manager, "1/kvm/0") +func (s *KVMSuite) TestCreateContainer(c *gc.C) { + instance := containertesting.CreateContainer(c, s.manager, "1/kvm/0") name := string(instance.Id()) cloudInitFilename := filepath.Join(s.ContainerDir, name, "cloud-init") containertesting.AssertCloudInit(c, cloudInitFilename) } -func (s *KVMSuite) TestStopContainer(c *gc.C) { - instance := containertesting.StartContainer(c, s.manager, "1/lxc/0") +func (s *KVMSuite) TestDestroyContainer(c *gc.C) { + instance := containertesting.CreateContainer(c, s.manager, "1/lxc/0") - err := s.manager.StopContainer(instance) + err := s.manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) name := string(instance.Id()) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/live_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/live_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/live_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/live_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "os" "runtime" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -52,8 +52,8 @@ func (s *LiveSuite) newManager(c *gc.C, name string) container.Manager { manager, err := kvm.NewContainerManager( container.ManagerConfig{ - Name: name, - LogDir: c.MkDir(), + container.ConfigName: name, + container.ConfigLogDir: c.MkDir(), }) c.Assert(err, gc.IsNil) return manager @@ -75,13 +75,13 @@ instances, err := manager.ListContainers() c.Assert(err, gc.IsNil) for _, instance := range instances { - err := manager.StopContainer(instance) + err := manager.DestroyContainer(instance) c.Check(err, gc.IsNil) } } } -func startContainer(c *gc.C, manager container.Manager, machineId string) instance.Instance { +func createContainer(c *gc.C, manager container.Manager, machineId string) instance.Instance { machineNonce := "fake-nonce" stateInfo := jujutesting.FakeStateInfo(machineId) apiInfo := jujutesting.FakeAPIInfo(machineId) @@ -96,7 +96,7 @@ err := environs.FinishMachineConfig(machineConfig, environConfig, constraints.Value{}) c.Assert(err, gc.IsNil) - inst, hardware, err := manager.StartContainer(machineConfig, "precise", network) + inst, hardware, err := manager.CreateContainer(machineConfig, "precise", network) c.Assert(err, gc.IsNil) c.Assert(hardware, gc.NotNil) expected := fmt.Sprintf("arch=%s cpu-cores=1 mem=512M root-disk=8192M", version.Current.Arch) @@ -106,8 +106,8 @@ func (s *LiveSuite) TestShutdownMachines(c *gc.C) { manager := s.newManager(c, "test") - startContainer(c, manager, "1/kvm/0") - startContainer(c, manager, "1/kvm/1") + createContainer(c, manager, "1/kvm/0") + createContainer(c, manager, "1/kvm/1") assertNumberOfContainers(c, manager, 2) shutdownMachines(manager)(c) @@ -118,13 +118,13 @@ firstManager := s.newManager(c, "first") s.AddCleanup(shutdownMachines(firstManager)) - startContainer(c, firstManager, "1/kvm/0") - startContainer(c, firstManager, "1/kvm/1") + createContainer(c, firstManager, "1/kvm/0") + createContainer(c, firstManager, "1/kvm/1") secondManager := s.newManager(c, "second") s.AddCleanup(shutdownMachines(secondManager)) - startContainer(c, secondManager, "1/kvm/0") + createContainer(c, secondManager, "1/kvm/0") assertNumberOfContainers(c, firstManager, 2) assertNumberOfContainers(c, secondManager, 1) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/mock/mock-kvm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/mock/mock-kvm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/kvm/mock/mock-kvm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/kvm/mock/mock-kvm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,11 @@ package mock_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/container/kvm" "launchpad.net/juju-core/container/kvm/mock" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/clonetemplate.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/clonetemplate.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/clonetemplate.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/clonetemplate.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,219 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package lxc + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + + "launchpad.net/golxc" + + coreCloudinit "launchpad.net/juju-core/cloudinit" + "launchpad.net/juju-core/container" + "launchpad.net/juju-core/environs/cloudinit" + "launchpad.net/juju-core/juju/osenv" + "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/utils/fslock" + "launchpad.net/juju-core/utils/tailer" +) + +const ( + templateShutdownUpstartFilename = "/etc/init/juju-template-restart.conf" + templateShutdownUpstartScript = ` +description "Juju lxc template shutdown job" +author "Juju Team " +start on stopped cloud-final + +script + shutdown -h now +end script + +post-stop script + rm ` + templateShutdownUpstartFilename + ` +end script +` +) + +var ( + TemplateLockDir = "/var/lib/juju/locks" + + TemplateStopTimeout = 5 * time.Minute +) + +// templateUserData returns a minimal user data necessary for the template. +// This should have the authorized keys, base packages, the cloud archive if +// necessary, initial apt proxy config, and it should do the apt-get +// update/upgrade initially. +func templateUserData( + series string, + authorizedKeys string, + aptProxy osenv.ProxySettings, +) ([]byte, error) { + config := coreCloudinit.New() + config.AddScripts( + "set -xe", // ensure we run all the scripts or abort. + ) + config.AddSSHAuthorizedKeys(authorizedKeys) + cloudinit.MaybeAddCloudArchiveCloudTools(config, series) + cloudinit.AddAptCommands(aptProxy, config) + config.AddScripts( + fmt.Sprintf( + "printf '%%s\n' %s > %s", + utils.ShQuote(templateShutdownUpstartScript), + templateShutdownUpstartFilename, + )) + data, err := config.Render() + if err != nil { + return nil, err + } + return data, nil +} + +func AcquireTemplateLock(name, message string) (*fslock.Lock, error) { + logger.Infof("wait for fslock on %v", name) + lock, err := fslock.NewLock(TemplateLockDir, name) + if err != nil { + logger.Tracef("failed to create fslock for template: %v", err) + return nil, err + } + err = lock.Lock(message) + if err != nil { + logger.Tracef("failed to acquire lock for template: %v", err) + return nil, err + } + return lock, nil +} + +// Make sure a template exists that we can clone from. +func EnsureCloneTemplate( + backingFilesystem string, + series string, + network *container.NetworkConfig, + authorizedKeys string, + aptProxy osenv.ProxySettings, +) (golxc.Container, error) { + name := fmt.Sprintf("juju-%s-template", series) + containerDirectory, err := container.NewDirectory(name) + if err != nil { + return nil, err + } + + lock, err := AcquireTemplateLock(name, "ensure clone exists") + if err != nil { + return nil, err + } + defer lock.Unlock() + + lxcContainer := LxcObjectFactory.New(name) + // Early exit if the container has been constructed before. + if lxcContainer.IsConstructed() { + logger.Infof("template exists, continuing") + return lxcContainer, nil + } + logger.Infof("template does not exist, creating") + + userData, err := templateUserData(series, authorizedKeys, aptProxy) + if err != nil { + logger.Tracef("filed to create tempalte user data for template: %v", err) + return nil, err + } + userDataFilename, err := container.WriteCloudInitFile(containerDirectory, userData) + if err != nil { + return nil, err + } + + configFile, err := writeLxcConfig(network, containerDirectory) + if err != nil { + logger.Errorf("failed to write config file: %v", err) + return nil, err + } + templateParams := []string{ + "--debug", // Debug errors in the cloud image + "--userdata", userDataFilename, // Our groovey cloud-init + "--hostid", name, // Use the container name as the hostid + "-r", series, + } + var extraCreateArgs []string + if backingFilesystem == Btrfs { + extraCreateArgs = append(extraCreateArgs, "-B", Btrfs) + } + // Create the container. + logger.Tracef("create the container") + if err := lxcContainer.Create(configFile, defaultTemplate, extraCreateArgs, templateParams); err != nil { + logger.Errorf("lxc container creation failed: %v", err) + return nil, err + } + // Make sure that the mount dir has been created. + logger.Tracef("make the mount dir for the shared logs") + if err := os.MkdirAll(internalLogDir(name), 0755); err != nil { + logger.Tracef("failed to create internal /var/log/juju mount dir: %v", err) + return nil, err + } + + // Start the lxc container with the appropriate settings for grabbing the + // console output and a log file. + consoleFile := filepath.Join(containerDirectory, "console.log") + lxcContainer.SetLogFile(filepath.Join(containerDirectory, "container.log"), golxc.LogDebug) + logger.Tracef("start the container") + // We explicitly don't pass through the config file to the container.Start + // method as we have passed it through at container creation time. This + // is necessary to get the appropriate rootfs reference without explicitly + // setting it ourselves. + if err = lxcContainer.Start("", consoleFile); err != nil { + logger.Errorf("container failed to start: %v", err) + return nil, err + } + logger.Infof("template container started, now wait for it to stop") + // Perhaps we should wait for it to finish, and the question becomes "how + // long do we wait for it to complete?" + + console, err := os.Open(consoleFile) + if err != nil { + // can't listen + return nil, err + } + + tailWriter := &logTail{tick: time.Now()} + consoleTailer := tailer.NewTailer(console, tailWriter, 0, nil) + defer consoleTailer.Stop() + + // We should wait maybe 1 minute between output? + // if no output check to see if stopped + // If we have no output and still running, something has probably gone wrong + for lxcContainer.IsRunning() { + if tailWriter.lastTick().Before(time.Now().Add(-TemplateStopTimeout)) { + logger.Infof("not heard anything from the template log for five minutes") + return nil, fmt.Errorf("template container %q did not stop", name) + } + time.Sleep(time.Second) + } + + return lxcContainer, nil +} + +type logTail struct { + tick time.Time + mutex sync.Mutex +} + +var _ io.Writer = (*logTail)(nil) + +func (t *logTail) Write(data []byte) (int, error) { + logger.Tracef(string(data)) + t.mutex.Lock() + defer t.mutex.Unlock() + t.tick = time.Now() + return len(data), nil +} + +func (t *logTail) lastTick() time.Time { + t.mutex.Lock() + defer t.mutex.Unlock() + tick := t.tick + return tick +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,9 @@ package lxc var ( - NetworkConfigTemplate = networkConfigTemplate - GenerateNetworkConfig = generateNetworkConfig - RestartSymlink = restartSymlink + ContainerConfigFilename = containerConfigFilename + ContainerDirFilesystem = containerDirFilesystem + GenerateNetworkConfig = generateNetworkConfig + NetworkConfigTemplate = networkConfigTemplate + RestartSymlink = restartSymlink ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/initialisation.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/initialisation.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/initialisation.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/initialisation.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,22 +12,34 @@ "lxc", } -type containerInitialiser struct{} +type containerInitialiser struct { + series string +} // containerInitialiser implements container.Initialiser. var _ container.Initialiser = (*containerInitialiser)(nil) // NewContainerInitialiser returns an instance used to perform the steps // required to allow a host machine to run a LXC container. -func NewContainerInitialiser() container.Initialiser { - return &containerInitialiser{} +func NewContainerInitialiser(series string) container.Initialiser { + return &containerInitialiser{series} } // Initialise is specified on the container.Initialiser interface. func (ci *containerInitialiser) Initialise() error { - return ensureDependencies() + return ensureDependencies(ci.series) } -func ensureDependencies() error { - return utils.AptGetInstall(requiredPackages...) +// ensureDependencies creates a set of install packages using AptGetPreparePackages +// and runs each set of packages through AptGetInstall +func ensureDependencies(series string) error { + var err error + aptGetInstallCommandList := utils.AptGetPreparePackages(requiredPackages, series) + for _, commands := range aptGetInstallCommandList { + err = utils.AptGetInstall(commands...) + if err != nil { + return err + } + } + return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/initialisation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/initialisation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/initialisation_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/initialisation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,45 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package lxc + +import ( + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" +) + +type InitialiserSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&InitialiserSuite{}) + +func (s *InitialiserSuite) TestLTSSeriesPackages(c *gc.C) { + cmdChan := s.HookCommandOutput(&utils.AptCommandOutput, []byte{}, nil) + container := NewContainerInitialiser("precise") + err := container.Initialise() + c.Assert(err, gc.IsNil) + + cmd := <-cmdChan + c.Assert(cmd.Args, gc.DeepEquals, []string{ + "apt-get", "--option=Dpkg::Options::=--force-confold", + "--option=Dpkg::options::=--force-unsafe-io", "--assume-yes", "--quiet", + "install", "--target-release", "precise-updates/cloud-tools", "lxc", + }) +} + +func (s *InitialiserSuite) TestNoSeriesPackages(c *gc.C) { + cmdChan := s.HookCommandOutput(&utils.AptCommandOutput, []byte{}, nil) + container := NewContainerInitialiser("") + err := container.Initialise() + c.Assert(err, gc.IsNil) + + cmd := <-cmdChan + c.Assert(cmd.Args, gc.DeepEquals, []string{ + "apt-get", "--option=Dpkg::Options::=--force-confold", + "--option=Dpkg::options::=--force-unsafe-io", "--assume-yes", "--quiet", + "install", "lxc", + }) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/lxc.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/lxc.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/lxc.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/lxc.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,12 +7,16 @@ "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" + "strconv" "strings" + "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/golxc" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/container" "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/instance" @@ -32,6 +36,8 @@ const ( // DefaultLxcBridge is the package created container bridge DefaultLxcBridge = "lxcbr0" + // Btrfs is special as we treat it differently for create and clone. + Btrfs = "btrfs" ) // DefaultNetworkConfig returns a valid NetworkConfig to use the @@ -40,9 +46,31 @@ return container.BridgeNetworkConfig(DefaultLxcBridge) } +// FsCommandOutput calls cmd.Output, this is used as an overloading point so +// we can test what *would* be run without actually executing another program +var FsCommandOutput = (*exec.Cmd).CombinedOutput + +func containerDirFilesystem() (string, error) { + cmd := exec.Command("df", "--output=fstype", LxcContainerDir) + out, err := FsCommandOutput(cmd) + if err != nil { + return "", err + } + // The filesystem is the second line. + lines := strings.Split(string(out), "\n") + if len(lines) < 2 { + logger.Errorf("unexpected output: %q", out) + return "", fmt.Errorf("could not determine filesystem type") + } + return lines[1], nil +} + type containerManager struct { - name string - logdir string + name string + logdir string + createWithClone bool + useAUFS bool + backingFilesystem string } // containerManager implements container.Manager. @@ -51,76 +79,131 @@ // NewContainerManager returns a manager object that can start and stop lxc // containers. The containers that are created are namespaced by the name // parameter. -func NewContainerManager(conf container.ManagerConfig) container.Manager { - logdir := "/var/log/juju" - if conf.LogDir != "" { - logdir = conf.LogDir - } - return &containerManager{name: conf.Name, logdir: logdir} +func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) { + name := conf.PopValue(container.ConfigName) + if name == "" { + return nil, fmt.Errorf("name is required") + } + logDir := conf.PopValue(container.ConfigLogDir) + if logDir == "" { + logDir = agent.DefaultLogDir + } + // Explicitly ignore the error result from ParseBool. + // If it fails to parse, the value is false, and this suits + // us fine. + useClone, _ := strconv.ParseBool(conf.PopValue("use-clone")) + useAUFS, _ := strconv.ParseBool(conf.PopValue("use-aufs")) + backingFS, err := containerDirFilesystem() + if err != nil { + // Especially in tests, or a bot, the lxc dir may not exist + // causing the test to fail. Since we only really care if the + // backingFS is 'btrfs' and we treat the rest the same, just + // call it 'unknown'. + backingFS = "unknown" + } + logger.Tracef("backing filesystem: %q", backingFS) + conf.WarnAboutUnused() + return &containerManager{ + name: name, + logdir: logDir, + createWithClone: useClone, + useAUFS: useAUFS, + backingFilesystem: backingFS, + }, nil } -func (manager *containerManager) StartContainer( +func (manager *containerManager) CreateContainer( machineConfig *cloudinit.MachineConfig, series string, - network *container.NetworkConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { - + network *container.NetworkConfig, +) (instance.Instance, *instance.HardwareCharacteristics, error) { + start := time.Now() name := names.MachineTag(machineConfig.MachineId) if manager.name != "" { name = fmt.Sprintf("%s-%s", manager.name, name) } - // Note here that the lxcObjectFacotry only returns a valid container - // object, and doesn't actually construct the underlying lxc container on - // disk. - lxcContainer := LxcObjectFactory.New(name) - // Create the cloud-init. directory, err := container.NewDirectory(name) if err != nil { return nil, nil, err } logger.Tracef("write cloud-init") + if manager.createWithClone { + // If we are using clone, disable the apt-get steps + machineConfig.DisablePackageCommands = true + } userDataFilename, err := container.WriteUserData(machineConfig, directory) if err != nil { logger.Errorf("failed to write user data: %v", err) return nil, nil, err } logger.Tracef("write the lxc.conf file") - configFile, err := writeLxcConfig(network, directory, manager.logdir) + configFile, err := writeLxcConfig(network, directory) if err != nil { logger.Errorf("failed to write config file: %v", err) return nil, nil, err } - templateParams := []string{ - "--debug", // Debug errors in the cloud image - "--userdata", userDataFilename, // Our groovey cloud-init - "--hostid", name, // Use the container name as the hostid - "-r", series, - } - // Create the container. - logger.Tracef("create the container") - if err := lxcContainer.Create(configFile, defaultTemplate, templateParams...); err != nil { - logger.Errorf("lxc container creation failed: %v", err) - return nil, nil, err + + var lxcContainer golxc.Container + if manager.createWithClone { + templateContainer, err := EnsureCloneTemplate( + manager.backingFilesystem, + series, + network, + machineConfig.AuthorizedKeys, + machineConfig.AptProxySettings, + ) + if err != nil { + return nil, nil, err + } + templateParams := []string{ + "--debug", // Debug errors in the cloud image + "--userdata", userDataFilename, // Our groovey cloud-init + "--hostid", name, // Use the container name as the hostid + } + var extraCloneArgs []string + if manager.backingFilesystem == Btrfs || manager.useAUFS { + extraCloneArgs = append(extraCloneArgs, "--snapshot") + } + if manager.backingFilesystem != Btrfs && manager.useAUFS { + extraCloneArgs = append(extraCloneArgs, "--backingstore", "aufs") + } + + lock, err := AcquireTemplateLock(templateContainer.Name(), "clone") + if err != nil { + return nil, nil, fmt.Errorf("failed to acquire lock on template: %v", err) + } + defer lock.Unlock() + lxcContainer, err = templateContainer.Clone(name, extraCloneArgs, templateParams) + if err != nil { + logger.Errorf("lxc container cloning failed: %v", err) + return nil, nil, err + } + } else { + // Note here that the lxcObjectFacotry only returns a valid container + // object, and doesn't actually construct the underlying lxc container on + // disk. + lxcContainer = LxcObjectFactory.New(name) + templateParams := []string{ + "--debug", // Debug errors in the cloud image + "--userdata", userDataFilename, // Our groovey cloud-init + "--hostid", name, // Use the container name as the hostid + "-r", series, + } + // Create the container. + logger.Tracef("create the container") + if err := lxcContainer.Create(configFile, defaultTemplate, nil, templateParams); err != nil { + logger.Errorf("lxc container creation failed: %v", err) + return nil, nil, err + } + logger.Tracef("lxc container created") } - // Make sure that the mount dir has been created. - logger.Tracef("make the mount dir for the shard logs") - if err := os.MkdirAll(internalLogDir(name), 0755); err != nil { - logger.Errorf("failed to create internal /var/log/juju mount dir: %v", err) + if err := autostartContainer(name); err != nil { return nil, nil, err } - logger.Tracef("lxc container created") - // Now symlink the config file into the restart directory, if it exists. - // This is for backwards compatiblity. From Trusty onwards, the auto start - // option should be set in the LXC config file, this is done in the networkConfigTemplate - // function below. - if useRestartDir() { - containerConfigFile := filepath.Join(LxcContainerDir, name, "config") - if err := os.Symlink(containerConfigFile, restartSymlink(name)); err != nil { - return nil, nil, err - } - logger.Tracef("auto-restart link created") + if err := mountHostLogDir(name, manager.logdir); err != nil { + return nil, nil, err } - // Start the lxc container with the appropriate settings for grabbing the // console output and a log file. consoleFile := filepath.Join(directory, "console.log") @@ -138,11 +221,56 @@ hardware := &instance.HardwareCharacteristics{ Arch: &arch, } - logger.Tracef("container started") + logger.Tracef("container %q started: %v", name, time.Now().Sub(start)) return &lxcInstance{lxcContainer, name}, hardware, nil } -func (manager *containerManager) StopContainer(instance instance.Instance) error { +func appendToContainerConfig(name, line string) error { + file, err := os.OpenFile( + containerConfigFilename(name), os.O_RDWR|os.O_APPEND, 0644) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(line) + return err +} + +func autostartContainer(name string) error { + // Now symlink the config file into the restart directory, if it exists. + // This is for backwards compatiblity. From Trusty onwards, the auto start + // option should be set in the LXC config file, this is done in the networkConfigTemplate + // function below. + if useRestartDir() { + if err := os.Symlink( + containerConfigFilename(name), + restartSymlink(name), + ); err != nil { + return err + } + logger.Tracef("auto-restart link created") + } else { + logger.Tracef("Setting auto start to true in lxc config.") + return appendToContainerConfig(name, "lxc.start.auto = 1\n") + } + return nil +} + +func mountHostLogDir(name, logDir string) error { + // Make sure that the mount dir has been created. + logger.Tracef("make the mount dir for the shared logs") + if err := os.MkdirAll(internalLogDir(name), 0755); err != nil { + logger.Errorf("failed to create internal /var/log/juju mount dir: %v", err) + return err + } + line := fmt.Sprintf( + "lxc.mount.entry=%s var/log/juju none defaults,bind 0 0\n", + logDir) + return appendToContainerConfig(name, line) +} + +func (manager *containerManager) DestroyContainer(instance instance.Instance) error { + start := time.Now() name := string(instance.Id()) lxcContainer := LxcObjectFactory.New(name) if useRestartDir() { @@ -157,7 +285,9 @@ return err } - return container.RemoveDirectory(name) + err := container.RemoveDirectory(name) + logger.Tracef("container %q stopped: %v", name, time.Now().Sub(start)) + return err } func (manager *containerManager) ListContainers() (result []instance.Instance, err error) { @@ -194,9 +324,9 @@ return filepath.Join(LxcRestartDir, name+".conf") } -const localConfig = `%s -lxc.mount.entry=%s var/log/juju none defaults,bind 0 0 -` +func containerConfigFilename(name string) string { + return filepath.Join(LxcContainerDir, name, "config") +} const networkTemplate = ` lxc.network.type = %s @@ -205,35 +335,32 @@ ` func networkConfigTemplate(networkType, networkLink string) string { - networkConfig := fmt.Sprintf(networkTemplate, networkType, networkLink) - if !useRestartDir() { - networkConfig += "lxc.start.auto = 1\n" - logger.Tracef("Setting auto start to true in lxc config.") - } - return networkConfig + return fmt.Sprintf(networkTemplate, networkType, networkLink) } func generateNetworkConfig(network *container.NetworkConfig) string { + var lxcConfig string if network == nil { logger.Warningf("network unspecified, using default networking config") network = DefaultNetworkConfig() } switch network.NetworkType { case container.PhysicalNetwork: - return networkConfigTemplate("phys", network.Device) + lxcConfig = networkConfigTemplate("phys", network.Device) default: logger.Warningf("Unknown network config type %q: using bridge", network.NetworkType) fallthrough case container.BridgeNetwork: - return networkConfigTemplate("veth", network.Device) + lxcConfig = networkConfigTemplate("veth", network.Device) } + + return lxcConfig } -func writeLxcConfig(network *container.NetworkConfig, directory, logdir string) (string, error) { +func writeLxcConfig(network *container.NetworkConfig, directory string) (string, error) { networkConfig := generateNetworkConfig(network) configFilename := filepath.Join(directory, "lxc.conf") - configContent := fmt.Sprintf(localConfig, networkConfig, logdir) - if err := ioutil.WriteFile(configFilename, []byte(configContent), 0644); err != nil { + if err := ioutil.WriteFile(configFilename, []byte(networkConfig), 0644); err != nil { return "", err } return configFilename, nil diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/lxc_test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/lxc_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/lxc_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/lxc_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,18 +10,22 @@ "path/filepath" "strings" stdtesting "testing" + "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/golxc" "launchpad.net/goyaml" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/container" "launchpad.net/juju-core/container/lxc" + "launchpad.net/juju-core/container/lxc/mock" lxctesting "launchpad.net/juju-core/container/lxc/testing" containertesting "launchpad.net/juju-core/container/testing" instancetest "launchpad.net/juju-core/instance/testing" - jc "launchpad.net/juju-core/testing/checkers" + "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/testing/testbase" ) @@ -31,6 +35,10 @@ type LxcSuite struct { lxctesting.TestSuite + + events chan mock.Event + useClone bool + useAUFS bool } var _ = gc.Suite(&LxcSuite{}) @@ -38,11 +46,76 @@ func (s *LxcSuite) SetUpTest(c *gc.C) { s.TestSuite.SetUpTest(c) loggo.GetLogger("juju.container.lxc").SetLogLevel(loggo.TRACE) + s.events = make(chan mock.Event, 25) + s.TestSuite.Factory.AddListener(s.events) + s.PatchValue(&lxc.TemplateLockDir, c.MkDir()) + s.PatchValue(&lxc.TemplateStopTimeout, 500*time.Millisecond) +} + +func (s *LxcSuite) TearDownTest(c *gc.C) { + s.TestSuite.Factory.RemoveListener(s.events) + close(s.events) + s.TestSuite.TearDownTest(c) +} + +func (s *LxcSuite) TestContainerDirFilesystem(c *gc.C) { + for i, test := range []struct { + message string + output string + expected string + errorMatch string + }{{ + message: "btrfs", + output: "Type\nbtrfs\n", + expected: lxc.Btrfs, + }, { + message: "ext4", + output: "Type\next4\n", + expected: "ext4", + }, { + message: "not enough output", + output: "foo", + errorMatch: "could not determine filesystem type", + }} { + c.Logf("%v: %s", i, test.message) + s.HookCommandOutput(&lxc.FsCommandOutput, []byte(test.output), nil) + value, err := lxc.ContainerDirFilesystem() + if test.errorMatch == "" { + c.Check(err, gc.IsNil) + c.Check(value, gc.Equals, test.expected) + } else { + c.Check(err, gc.ErrorMatches, test.errorMatch) + } + } } -func (s *LxcSuite) TestStartContainer(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") +func (s *LxcSuite) makeManager(c *gc.C, name string) container.Manager { + params := container.ManagerConfig{ + container.ConfigName: name, + } + if s.useClone { + params["use-clone"] = "true" + } + if s.useAUFS { + params["use-aufs"] = "true" + } + manager, err := lxc.NewContainerManager(params) + c.Assert(err, gc.IsNil) + return manager +} + +func (*LxcSuite) TestManagerWarnsAboutUnknownOption(c *gc.C) { + _, err := lxc.NewContainerManager(container.ManagerConfig{ + container.ConfigName: "BillyBatson", + "shazam": "Captain Marvel", + }) + c.Assert(err, gc.IsNil) + c.Assert(c.GetTestLog(), jc.Contains, `WARNING juju.container unused config option: "shazam" -> "Captain Marvel"`) +} + +func (s *LxcSuite) TestCreateContainer(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") name := string(instance.Id()) // Check our container config files. @@ -68,7 +141,7 @@ }) // Check the mount point has been created inside the container. - c.Assert(filepath.Join(s.LxcDir, name, "rootfs/var/log/juju"), jc.IsDirectory) + c.Assert(filepath.Join(s.LxcDir, name, "rootfs", agent.DefaultLogDir), jc.IsDirectory) // Check that the config file is linked in the restart dir. expectedLinkLocation := filepath.Join(s.RestartDir, name+".conf") expectedTarget := filepath.Join(s.LxcDir, name, "config") @@ -81,25 +154,136 @@ c.Assert(location, gc.Equals, expectedTarget) } +func (s *LxcSuite) ensureTemplateStopped(name string) { + go func() { + for { + template := s.Factory.New(name) + if template.IsRunning() { + template.Stop() + } + time.Sleep(50 * time.Millisecond) + } + }() +} + +func (s *LxcSuite) AssertEvent(c *gc.C, event mock.Event, expected mock.Action, id string) { + c.Assert(event.Action, gc.Equals, expected) + c.Assert(event.InstanceId, gc.Equals, id) +} + +func (s *LxcSuite) TestCreateContainerEvents(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1") + id := string(instance.Id()) + s.AssertEvent(c, <-s.events, mock.Created, id) + s.AssertEvent(c, <-s.events, mock.Started, id) +} + +func (s *LxcSuite) TestCreateContainerEventsWithClone(c *gc.C) { + s.PatchValue(&s.useClone, true) + // The template containers are created with an upstart job that + // stops them once cloud init has finished. We emulate that here. + template := "juju-series-template" + s.ensureTemplateStopped(template) + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1") + id := string(instance.Id()) + s.AssertEvent(c, <-s.events, mock.Created, template) + s.AssertEvent(c, <-s.events, mock.Started, template) + s.AssertEvent(c, <-s.events, mock.Stopped, template) + s.AssertEvent(c, <-s.events, mock.Cloned, template) + s.AssertEvent(c, <-s.events, mock.Started, id) +} + +func (s *LxcSuite) createTemplate(c *gc.C) golxc.Container { + name := "juju-series-template" + s.ensureTemplateStopped(name) + network := container.BridgeNetworkConfig("nic42") + authorizedKeys := "authorized keys list" + aptProxy := osenv.ProxySettings{} + template, err := lxc.EnsureCloneTemplate( + "ext4", "series", network, authorizedKeys, aptProxy) + c.Assert(err, gc.IsNil) + c.Assert(template.Name(), gc.Equals, name) + s.AssertEvent(c, <-s.events, mock.Created, name) + s.AssertEvent(c, <-s.events, mock.Started, name) + s.AssertEvent(c, <-s.events, mock.Stopped, name) + + autostartLink := lxc.RestartSymlink(name) + config, err := ioutil.ReadFile(lxc.ContainerConfigFilename(name)) + c.Assert(err, gc.IsNil) + expected := ` +lxc.network.type = veth +lxc.network.link = nic42 +lxc.network.flags = up +` + // NOTE: no autostart, no mounting the log dir + c.Assert(string(config), gc.Equals, expected) + c.Assert(autostartLink, jc.DoesNotExist) + + return template +} + +func (s *LxcSuite) TestCreateContainerEventsWithCloneExistingTemplate(c *gc.C) { + s.createTemplate(c) + s.PatchValue(&s.useClone, true) + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1") + name := string(instance.Id()) + cloned := <-s.events + s.AssertEvent(c, cloned, mock.Cloned, "juju-series-template") + c.Assert(cloned.Args, gc.IsNil) + s.AssertEvent(c, <-s.events, mock.Started, name) +} + +func (s *LxcSuite) TestCreateContainerEventsWithCloneExistingTemplateAUFS(c *gc.C) { + s.createTemplate(c) + s.PatchValue(&s.useClone, true) + s.PatchValue(&s.useAUFS, true) + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1") + name := string(instance.Id()) + cloned := <-s.events + s.AssertEvent(c, cloned, mock.Cloned, "juju-series-template") + c.Assert(cloned.Args, gc.DeepEquals, []string{"--snapshot", "--backingstore", "aufs"}) + s.AssertEvent(c, <-s.events, mock.Started, name) +} + +func (s *LxcSuite) TestCreateContainerWithCloneMountsAndAutostarts(c *gc.C) { + s.createTemplate(c) + s.PatchValue(&s.useClone, true) + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1") + name := string(instance.Id()) + + autostartLink := lxc.RestartSymlink(name) + config, err := ioutil.ReadFile(lxc.ContainerConfigFilename(name)) + c.Assert(err, gc.IsNil) + mountLine := "lxc.mount.entry=/var/log/juju var/log/juju none defaults,bind 0 0" + c.Assert(string(config), jc.Contains, mountLine) + c.Assert(autostartLink, jc.IsSymlink) +} + func (s *LxcSuite) TestContainerState(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") + manager := s.makeManager(c, "test") + c.Logf("%#v", manager) + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") // The mock container will be immediately "running". c.Assert(instance.Status(), gc.Equals, string(golxc.StateRunning)) - // StopContainer stops and then destroys the container, putting it + // DestroyContainer stops and then destroys the container, putting it // into "unknown" state. - err := manager.StopContainer(instance) + err := manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) c.Assert(instance.Status(), gc.Equals, string(golxc.StateUnknown)) } -func (s *LxcSuite) TestStopContainer(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") +func (s *LxcSuite) TestDestroyContainer(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") - err := manager.StopContainer(instance) + err := manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) name := string(instance.Id()) @@ -109,16 +293,16 @@ c.Assert(filepath.Join(s.RemovedDir, name), jc.IsDirectory) } -func (s *LxcSuite) TestStopContainerNameClash(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") +func (s *LxcSuite) TestDestroyContainerNameClash(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") name := string(instance.Id()) targetDir := filepath.Join(s.RemovedDir, name) err := os.MkdirAll(targetDir, 0755) c.Assert(err, gc.IsNil) - err = manager.StopContainer(instance) + err = manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) // Check that the container dir is no longer in the container dir @@ -128,21 +312,21 @@ } func (s *LxcSuite) TestNamedManagerPrefix(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{Name: "eric"}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") + manager := s.makeManager(c, "eric") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") c.Assert(string(instance.Id()), gc.Equals, "eric-machine-1-lxc-0") } func (s *LxcSuite) TestListContainers(c *gc.C) { - foo := lxc.NewContainerManager(container.ManagerConfig{Name: "foo"}) - bar := lxc.NewContainerManager(container.ManagerConfig{Name: "bar"}) + foo := s.makeManager(c, "foo") + bar := s.makeManager(c, "bar") - foo1 := containertesting.StartContainer(c, foo, "1/lxc/0") - foo2 := containertesting.StartContainer(c, foo, "1/lxc/1") - foo3 := containertesting.StartContainer(c, foo, "1/lxc/2") + foo1 := containertesting.CreateContainer(c, foo, "1/lxc/0") + foo2 := containertesting.CreateContainer(c, foo, "1/lxc/1") + foo3 := containertesting.CreateContainer(c, foo, "1/lxc/2") - bar1 := containertesting.StartContainer(c, bar, "1/lxc/0") - bar2 := containertesting.StartContainer(c, bar, "1/lxc/1") + bar1 := containertesting.CreateContainer(c, bar, "1/lxc/0") + bar2 := containertesting.CreateContainer(c, bar, "1/lxc/1") result, err := foo.ListContainers() c.Assert(err, gc.IsNil) @@ -153,48 +337,50 @@ instancetest.MatchInstances(c, result, bar1, bar2) } -func (s *LxcSuite) TestStartContainerAutostarts(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") +func (s *LxcSuite) TestCreateContainerAutostarts(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") autostartLink := lxc.RestartSymlink(string(instance.Id())) c.Assert(autostartLink, jc.IsSymlink) } -func (s *LxcSuite) TestStartContainerNoRestartDir(c *gc.C) { +func (s *LxcSuite) TestCreateContainerNoRestartDir(c *gc.C) { err := os.Remove(s.RestartDir) c.Assert(err, gc.IsNil) - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") - autostartLink := lxc.RestartSymlink(string(instance.Id())) - - config := lxc.NetworkConfigTemplate("foo", "bar") + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") + name := string(instance.Id()) + autostartLink := lxc.RestartSymlink(name) + config, err := ioutil.ReadFile(lxc.ContainerConfigFilename(name)) + c.Assert(err, gc.IsNil) expected := ` -lxc.network.type = foo -lxc.network.link = bar +lxc.network.type = veth +lxc.network.link = nic42 lxc.network.flags = up lxc.start.auto = 1 +lxc.mount.entry=/var/log/juju var/log/juju none defaults,bind 0 0 ` - c.Assert(config, gc.Equals, expected) + c.Assert(string(config), gc.Equals, expected) c.Assert(autostartLink, jc.DoesNotExist) } -func (s *LxcSuite) TestStopContainerRemovesAutostartLink(c *gc.C) { - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") - err := manager.StopContainer(instance) +func (s *LxcSuite) TestDestroyContainerRemovesAutostartLink(c *gc.C) { + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") + err := manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) autostartLink := lxc.RestartSymlink(string(instance.Id())) c.Assert(autostartLink, jc.SymlinkDoesNotExist) } -func (s *LxcSuite) TestStopContainerNoRestartDir(c *gc.C) { +func (s *LxcSuite) TestDestroyContainerNoRestartDir(c *gc.C) { err := os.Remove(s.RestartDir) c.Assert(err, gc.IsNil) - manager := lxc.NewContainerManager(container.ManagerConfig{}) - instance := containertesting.StartContainer(c, manager, "1/lxc/0") - err = manager.StopContainer(instance) + manager := s.makeManager(c, "test") + instance := containertesting.CreateContainer(c, manager, "1/lxc/0") + err = manager.DestroyContainer(instance) c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/mock/mock-lxc.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/mock/mock-lxc.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/mock/mock-lxc.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/mock/mock-lxc.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,13 +5,23 @@ import ( "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "github.com/juju/loggo" "launchpad.net/golxc" + + "launchpad.net/juju-core/container" + "launchpad.net/juju-core/utils" ) // This file provides a mock implementation of the golxc interfaces // ContainerFactory and Container. +var logger = loggo.GetLogger("juju.container.lxc.mock") + type Action int const ( @@ -19,6 +29,12 @@ Started Action = iota // A container has been stopped. Stopped + // A container has been created. + Created + // A container has been destroyed. + Destroyed + // A container has been cloned. + Cloned ) func (action Action) String() string { @@ -27,13 +43,21 @@ return "Started" case Stopped: return "Stopped" + case Created: + return "Created" + case Destroyed: + return "Destroyed" + case Cloned: + return "Cloned" } return "unknown" } type Event struct { - Action Action - InstanceId string + Action Action + InstanceId string + Args []string + TemplateArgs []string } type ContainerFactory interface { @@ -44,13 +68,16 @@ } type mockFactory struct { - instances map[string]golxc.Container - listeners []chan<- Event + containerDir string + instances map[string]golxc.Container + listeners []chan<- Event + mutex sync.Mutex } -func MockFactory() ContainerFactory { +func MockFactory(containerDir string) ContainerFactory { return &mockFactory{ - instances: make(map[string]golxc.Container), + containerDir: containerDir, + instances: make(map[string]golxc.Container), } } @@ -62,47 +89,85 @@ logLevel golxc.LogLevel } +func (mock *mockContainer) getState() golxc.State { + mock.factory.mutex.Lock() + defer mock.factory.mutex.Unlock() + return mock.state +} + +func (mock *mockContainer) setState(newState golxc.State) { + mock.factory.mutex.Lock() + defer mock.factory.mutex.Unlock() + mock.state = newState + logger.Debugf("container %q state change to %s", mock.name, string(newState)) +} + // Name returns the name of the container. func (mock *mockContainer) Name() string { return mock.name } +func (mock *mockContainer) configFilename() string { + return filepath.Join(mock.factory.containerDir, mock.name, "config") +} + // Create creates a new container based on the given template. -func (mock *mockContainer) Create(configFile, template string, templateArgs ...string) error { - if mock.state != golxc.StateUnknown { +func (mock *mockContainer) Create(configFile, template string, extraArgs []string, templateArgs []string) error { + if mock.getState() != golxc.StateUnknown { return fmt.Errorf("container is already created") } - mock.state = golxc.StateStopped mock.factory.instances[mock.name] = mock + // Create the container directory. + containerDir := filepath.Join(mock.factory.containerDir, mock.name) + if err := os.MkdirAll(containerDir, 0755); err != nil { + return err + } + if err := utils.CopyFile(mock.configFilename(), configFile); err != nil { + return err + } + mock.setState(golxc.StateStopped) + mock.factory.notify(eventArgs(Created, mock.name, extraArgs, templateArgs)) return nil } // Start runs the container as a daemon. func (mock *mockContainer) Start(configFile, consoleFile string) error { - if mock.state == golxc.StateUnknown { + state := mock.getState() + if state == golxc.StateUnknown { return fmt.Errorf("container has not been created") - } else if mock.state == golxc.StateRunning { + } else if state == golxc.StateRunning { return fmt.Errorf("container is already running") } - mock.state = golxc.StateRunning - mock.factory.notify(Started, mock.name) + ioutil.WriteFile( + filepath.Join(container.ContainerDir, mock.name, "console.log"), + []byte("fake console.log"), 0644) + mock.setState(golxc.StateRunning) + mock.factory.notify(event(Started, mock.name)) return nil } // Stop terminates the running container. func (mock *mockContainer) Stop() error { - if mock.state == golxc.StateUnknown { + state := mock.getState() + if state == golxc.StateUnknown { return fmt.Errorf("container has not been created") - } else if mock.state == golxc.StateStopped { + } else if state == golxc.StateStopped { return fmt.Errorf("container is already stopped") } - mock.state = golxc.StateStopped - mock.factory.notify(Stopped, mock.name) + mock.setState(golxc.StateStopped) + mock.factory.notify(event(Stopped, mock.name)) return nil } // Clone creates a copy of the container, giving the copy the specified name. -func (mock *mockContainer) Clone(name string) (golxc.Container, error) { +func (mock *mockContainer) Clone(name string, extraArgs []string, templateArgs []string) (golxc.Container, error) { + state := mock.getState() + if state == golxc.StateUnknown { + return nil, fmt.Errorf("container has not been created") + } else if state == golxc.StateRunning { + return nil, fmt.Errorf("container is running, clone not possible") + } + container := &mockContainer{ factory: mock.factory, name: name, @@ -110,6 +175,17 @@ logLevel: golxc.LogWarning, } mock.factory.instances[name] = container + + // Create the container directory. + containerDir := filepath.Join(mock.factory.containerDir, name) + if err := os.MkdirAll(containerDir, 0755); err != nil { + return nil, err + } + if err := utils.CopyFile(container.configFilename(), mock.configFilename()); err != nil { + return nil, err + } + + mock.factory.notify(eventArgs(Cloned, mock.name, extraArgs, templateArgs)) return container, nil } @@ -125,15 +201,17 @@ // Destroy stops and removes the container. func (mock *mockContainer) Destroy() error { + state := mock.getState() // golxc destroy will stop the machine if it is running. - if mock.state == golxc.StateRunning { + if state == golxc.StateRunning { mock.Stop() } - if mock.state == golxc.StateUnknown { + if state == golxc.StateUnknown { return fmt.Errorf("container has not been created") } - mock.state = golxc.StateUnknown delete(mock.factory.instances, mock.name) + mock.setState(golxc.StateUnknown) + mock.factory.notify(event(Destroyed, mock.name)) return nil } @@ -145,27 +223,28 @@ // Info returns the status and the process id of the container. func (mock *mockContainer) Info() (golxc.State, int, error) { pid := -1 - if mock.state == golxc.StateRunning { + state := mock.getState() + if state == golxc.StateRunning { pid = 42 } - return mock.state, pid, nil + return state, pid, nil } // IsConstructed checks if the container image exists. func (mock *mockContainer) IsConstructed() bool { - return mock.state != golxc.StateUnknown + return mock.getState() != golxc.StateUnknown } // IsRunning checks if the state of the container is 'RUNNING'. func (mock *mockContainer) IsRunning() bool { - return mock.state == golxc.StateRunning + return mock.getState() == golxc.StateRunning } // String returns information about the container, like the name, state, // and process id. func (mock *mockContainer) String() string { - _, pid, _ := mock.Info() - return fmt.Sprintf("", mock.name, string(mock.state), pid) + state, pid, _ := mock.Info() + return fmt.Sprintf("", mock.name, string(state), pid) } // LogFile returns the current filename used for the LogFile. @@ -190,6 +269,8 @@ } func (mock *mockFactory) New(name string) golxc.Container { + mock.mutex.Lock() + defer mock.mutex.Unlock() container, ok := mock.instances[name] if ok { return container @@ -200,6 +281,7 @@ state: golxc.StateUnknown, logLevel: golxc.LogWarning, } + mock.instances[name] = container return container } @@ -210,8 +292,15 @@ return } -func (mock *mockFactory) notify(action Action, instanceId string) { - event := Event{action, instanceId} +func event(action Action, instanceId string) Event { + return Event{action, instanceId, nil, nil} +} + +func eventArgs(action Action, instanceId string, args []string, template []string) Event { + return Event{action, instanceId, args, template} +} + +func (mock *mockFactory) notify(event Event) { for _, c := range mock.listeners { c <- event } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/testing/test.go juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/testing/test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/lxc/testing/test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/lxc/testing/test.go 2014-03-20 12:52:38.000000000 +0000 @@ -33,6 +33,6 @@ s.PatchValue(&lxc.LxcContainerDir, s.LxcDir) s.RestartDir = c.MkDir() s.PatchValue(&lxc.LxcRestartDir, s.RestartDir) - s.Factory = mock.MockFactory() + s.Factory = mock.MockFactory(s.LxcDir) s.PatchValue(&lxc.LxcObjectFactory, s.Factory) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/testing/common.go juju-core-1.17.6/src/launchpad.net/juju-core/container/testing/common.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/testing/common.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/testing/common.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,18 +6,18 @@ import ( "io/ioutil" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/container" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/instance" jujutesting "launchpad.net/juju-core/juju/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) -func StartContainer(c *gc.C, manager container.Manager, machineId string) instance.Instance { +func CreateContainer(c *gc.C, manager container.Manager, machineId string) instance.Instance { stateInfo := jujutesting.FakeStateInfo(machineId) apiInfo := jujutesting.FakeAPIInfo(machineId) machineConfig := environs.NewMachineConfig(machineId, "fake-nonce", stateInfo, apiInfo) @@ -28,7 +28,7 @@ series := "series" network := container.BridgeNetworkConfig("nic42") - inst, hardware, err := manager.StartContainer(machineConfig, series, network) + inst, hardware, err := manager.CreateContainer(machineConfig, series, network) c.Assert(err, gc.IsNil) c.Assert(hardware, gc.NotNil) c.Assert(hardware.String(), gc.Not(gc.Equals), "") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/container/userdata.go juju-core-1.17.6/src/launchpad.net/juju-core/container/userdata.go --- juju-core-1.17.4/src/launchpad.net/juju-core/container/userdata.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/container/userdata.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "io/ioutil" "path/filepath" - "github.com/loggo/loggo" + "github.com/juju/loggo" coreCloudinit "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/environs/cloudinit" @@ -17,12 +17,21 @@ logger = loggo.GetLogger("juju.container") ) +// WriteUserData generates the cloud init for the specified machine config, +// and writes the serialized form out to a cloud-init file in the directory +// specified. func WriteUserData(machineConfig *cloudinit.MachineConfig, directory string) (string, error) { userData, err := cloudInitUserData(machineConfig) if err != nil { logger.Errorf("failed to create user data: %v", err) return "", err } + return WriteCloudInitFile(directory, userData) +} + +// WriteCloudInitFile writes the data out to a cloud-init file in the +// directory specified, and returns the filename. +func WriteCloudInitFile(directory string, userData []byte) (string, error) { userDataFilename := filepath.Join(directory, "cloud-init") if err := ioutil.WriteFile(userDataFilename, userData, 0644); err != nil { logger.Errorf("failed to write user data: %v", err) @@ -32,8 +41,6 @@ } func cloudInitUserData(machineConfig *cloudinit.MachineConfig) ([]byte, error) { - // consider not having this line hardcoded... - machineConfig.DataDir = "/var/lib/juju" cloudConfig := coreCloudinit.New() err := cloudinit.Configure(machineConfig, cloudConfig) if err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/CONTRIBUTING juju-core-1.17.6/src/launchpad.net/juju-core/CONTRIBUTING --- juju-core-1.17.4/src/launchpad.net/juju-core/CONTRIBUTING 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/CONTRIBUTING 2014-03-20 12:52:38.000000000 +0000 @@ -82,7 +82,7 @@ "labix.org/v2/mgo" gc "launchpad.net/gocheck" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/state" "launchpad.net/juju-core/worker" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/dependencies.tsv juju-core-1.17.6/src/launchpad.net/juju-core/dependencies.tsv --- juju-core-1.17.4/src/launchpad.net/juju-core/dependencies.tsv 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/dependencies.tsv 2014-03-20 12:52:38.000000000 +0000 @@ -1,12 +1,14 @@ code.google.com/p/go.crypto hg 6478cc9340cbbe6c04511280c5007722269108e9 184 code.google.com/p/go.net hg 3591c18acabc99439c783463ef00e6dc277eee39 77 -labix.org/v2/mgo bzr gustavo@niemeyer.net-20131118213720-aralgr4ienh0gdyq 248 github.com/errgo/errgo git 93d72bf813883d1054cae1c001d3a46603f7f559 -github.com/loggo/loggo git 89458b4dc99692bc24efe9c2252d7587f8dc247b +github.com/juju/loggo git fa3acf9ab9ed09aea29030558528e24a254d27af +github.com/juju/ratelimit git 0025ab75db6c6eaa4ffff0240c2c9e617ad1a0eb +github.com/juju/testing git 9c0e0686136637876ae659e9056897575236e11f +labix.org/v2/mgo bzr gustavo@niemeyer.net-20131118213720-aralgr4ienh0gdyq 248 launchpad.net/gnuflag bzr roger.peppe@canonical.com-20121003093437-zcyyw0lpvj2nifpk 12 launchpad.net/goamz bzr roger.peppe@canonical.com-20131218155244-hbnkvlkkzy3vmlh9 44 launchpad.net/gocheck bzr gustavo@niemeyer.net-20130302024745-6ikofwq2c03h7giu 85 -launchpad.net/golxc bzr ian.booth@canonical.com-20140116225718-lvoikrb0me6zugin 6 +launchpad.net/golxc bzr tim.penhey@canonical.com-20140311005930-b14361bwnocu3krh 8 launchpad.net/gomaasapi bzr ian.booth@canonical.com-20131017011445-m1hmr0ap14osd7li 47 launchpad.net/goose bzr tarmac-20140124165235-h9rloooc531udms5 116 launchpad.net/goyaml bzr gustavo@niemeyer.net-20131114120802-abe042syx64z2m7s 50 diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/boilerplate_config.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/boilerplate_config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/boilerplate_config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/boilerplate_config.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,24 +12,36 @@ ) var configHeader = ` -# This is the Juju config file, which you can use to specify multiple environments in which to deploy. -# By default Juju ships AWS (default), HP Cloud, OpenStack. -# See https://juju.ubuntu.com/docs for more information +# This is the Juju config file, which you can use to specify multiple +# environments in which to deploy. By default Juju ships with AWS +# (default), HP Cloud, OpenStack, Azure, MaaS, Local and Manual +# providers. See https://juju.ubuntu.com/docs for more information -# An environment configuration must always specify at least the following information: -# +# An environment configuration must always specify at least the +# following information: # - name (to identify the environment) # - type (to specify the provider) +# In the following example the name is "myenv" and type is "ec2". +# myenv: +# type: ec2 # Values in below need to be filled in by the user. # Optional attributes are shown commented out, with # a sample value or a value in . +# There are several settings supported by all environments, all of which +# are optional and have specified default values. For more info, see the +# Juju documentation. + # The default environment is chosen when an environment is not # specified using any of the following, in descending order of precedence: -# -e or --environment command line parameter. -# JUJU_ENV environment variable. -# the juju switch command. +# 1. -e or --environment command line parameter, passed after the command, e.g. +# $ juju add-unit -e myenv myservice +# 2. By setting JUJU_ENV environment variable. +# 3. Using the juju switch command like this: +# $ juju switch myenv +# + default: amazon environments: diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/bootstrap.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/bootstrap.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "fmt" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" @@ -71,8 +71,9 @@ return toolsList, nil } -// EnsureNotBootstrapped returns null if the environment is not bootstrapped, -// and an error if it is or if the function was not able to tell. +// EnsureNotBootstrapped returns nil if the environment is not +// bootstrapped, and an error if it is or if the function was not able +// to tell. func EnsureNotBootstrapped(env environs.Environ) error { _, err := LoadState(env.Storage()) // If there is no error loading the bootstrap state, then we are diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "strings" stdtesting "testing" gc "launchpad.net/gocheck" @@ -14,10 +15,13 @@ "launchpad.net/juju-core/environs/bootstrap" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/configstore" + "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" + "launchpad.net/juju-core/environs/sync" envtesting "launchpad.net/juju-core/environs/testing" envtools "launchpad.net/juju-core/environs/tools" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/provider/dummy" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" @@ -54,7 +58,7 @@ } func (s *bootstrapSuite) TestBootstrapNeedsSettings(c *gc.C) { - env := newEnviron("bar", noKeysDefined) + env := newEnviron("bar", noKeysDefined, nil) s.setDummyStorage(c, env) fixEnv := func(key string, value interface{}) { cfg, err := env.Config().Apply(map[string]interface{}{ @@ -88,7 +92,7 @@ } func (s *bootstrapSuite) TestBootstrapEmptyConstraints(c *gc.C) { - env := newEnviron("foo", useDefaultKeys) + env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) c.Assert(err, gc.IsNil) @@ -97,7 +101,7 @@ } func (s *bootstrapSuite) TestBootstrapSpecifiedConstraints(c *gc.C) { - env := newEnviron("foo", useDefaultKeys) + env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) cons := constraints.MustParse("cpu-cores=2 mem=4G") err := bootstrap.Bootstrap(coretesting.Context(c), env, cons) @@ -135,10 +139,18 @@ DefaultSeries: "precise", Arch: "i386", Expect: []version.Binary{envtesting.V120p32}, + }, { + Info: "dev cli has different arch to available", + Available: envtesting.V1all, + CliVersion: envtesting.V310qppc64, + DefaultSeries: "precise", + Expect: []version.Binary{envtesting.V3101qppc64}, }} func (s *bootstrapSuite) TestBootstrapTools(c *gc.C) { allTests := append(envtesting.BootstrapToolsTests, bootstrapSetAgentVersionTests...) + // version.Current is set in the loop so ensure it is restored later. + s.PatchValue(&version.Current, version.Current) for i, test := range allTests { c.Logf("\ntest %d: %s", i, test.Info) dummy.Reset() @@ -166,8 +178,9 @@ cons = constraints.MustParse("arch=" + test.Arch) } err = bootstrap.Bootstrap(coretesting.Context(c), env, cons) - if test.Err != nil { - c.Check(err, gc.ErrorMatches, ".*"+test.Err.Error()) + if test.Err != "" { + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Check(stripped, gc.Matches, ".*"+stripped) continue } else { c.Check(err, gc.IsNil) @@ -185,7 +198,7 @@ } func (s *bootstrapSuite) TestBootstrapNoTools(c *gc.C) { - env := newEnviron("foo", useDefaultKeys) + env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) err := bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) @@ -194,12 +207,191 @@ c.Assert(err, gc.IsNil) } -func (s *bootstrapSuite) TestEnsureToolsAvailability(c *gc.C) { - env := newEnviron("foo", useDefaultKeys) +func (s *bootstrapSuite) TestEnsureToolsAvailabilityIncompatibleHostArch(c *gc.C) { + // Host runs amd64, want ppc64 tools. + s.PatchValue(&arch.HostArch, func() string { + return "amd64" + }) + env := newEnviron("foo", useDefaultKeys, nil) + s.setDummyStorage(c, env) + envtesting.RemoveFakeTools(c, env.Storage()) + arch := "ppc64" + _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), &arch) + c.Assert(err, gc.NotNil) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, + gc.Matches, + `cannot upload bootstrap tools: cannot build tools for "ppc64" using a machine running on "amd64"`) +} + +func (s *bootstrapSuite) TestEnsureToolsAvailabilityIncompatibleTargetArch(c *gc.C) { + // Host runs ppc64, environment only supports amd64, arm64. + s.PatchValue(&arch.HostArch, func() string { + return "ppc64" + }) + env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) - c.Check(err, gc.ErrorMatches, ".*no tools available") + c.Assert(err, gc.NotNil) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, + gc.Matches, + `cannot upload bootstrap tools: environment "foo" of type dummy does not support instances running on "ppc64"`) +} + +func (s *bootstrapSuite) TestEnsureToolsAvailabilityAgentVersionAlreadySet(c *gc.C) { + // Can't upload tools if agent version already set. + env := newEnviron("foo", useDefaultKeys, map[string]interface{}{"agent-version": "1.16.0"}) + s.setDummyStorage(c, env) + envtesting.RemoveFakeTools(c, env.Storage()) + _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) + c.Assert(err, gc.NotNil) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, + gc.Matches, + "cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*") +} + +func (s *bootstrapSuite) TestEnsureToolsAvailabilityNonDevVersion(c *gc.C) { + // Can't upload tools for released versions. + s.PatchValue(&version.Current, version.MustParseBinary("1.18.0-trusty-arm64")) + env := newEnviron("foo", useDefaultKeys, nil) + s.setDummyStorage(c, env) + envtesting.RemoveFakeTools(c, env.Storage()) + _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) + c.Assert(err, gc.NotNil) + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, + gc.Matches, + "cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*") +} + +// getMockBuildTools returns a sync.BuildToolsTarballFunc implementation which generates +// a fake tools tarball. +func (s *bootstrapSuite) getMockBuildTools(c *gc.C) sync.BuildToolsTarballFunc { + toolsDir := c.MkDir() + return func(forceVersion *version.Number) (*sync.BuiltTools, error) { + // UploadFakeToolsVersions requires a storage to write to. + stor, err := filestorage.NewFileStorageWriter(toolsDir) + c.Assert(err, gc.IsNil) + vers := version.Current + if forceVersion != nil { + vers.Number = *forceVersion + } + versions := []version.Binary{vers} + uploadedTools, err := envtesting.UploadFakeToolsVersions(stor, versions...) + c.Assert(err, gc.IsNil) + agentTools := uploadedTools[0] + return &sync.BuiltTools{ + Dir: toolsDir, + StorageName: envtools.StorageName(vers), + Version: vers, + Size: agentTools.Size, + Sha256Hash: agentTools.SHA256, + }, nil + } +} + +func (s *bootstrapSuite) TestEnsureToolsAvailability(c *gc.C) { + existingToolsVersion := version.MustParseBinary("1.19.0-trusty-amd64") + s.PatchValue(&version.Current, existingToolsVersion) + env := newEnviron("foo", useDefaultKeys, nil) + s.setDummyStorage(c, env) + // At this point, as a result of setDummyStorage, env has tools for amd64 uploaded. + // Set version.Current to be arm64 to simulate a different CLI version. + cliVersion := version.Current + cliVersion.Arch = "arm64" + version.Current = cliVersion + s.PatchValue(&sync.BuildToolsTarball, s.getMockBuildTools(c)) + // Host runs arm64, environment supports arm64. + s.PatchValue(&arch.HostArch, func() string { + return "arm64" + }) + arch := "arm64" + agentTools, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), &arch) + c.Assert(err, gc.IsNil) + c.Assert(agentTools, gc.HasLen, 1) + expectedVers := version.Current + expectedVers.Number.Build++ + expectedVers.Series = env.Config().DefaultSeries() + c.Assert(agentTools[0].Version, gc.DeepEquals, expectedVers) +} + +func (s *bootstrapSuite) TestSeriesToUpload(c *gc.C) { + vers := version.Current + vers.Series = "quantal" + s.PatchValue(&version.Current, vers) + env := newEnviron("foo", useDefaultKeys, nil) + cfg := env.Config() + c.Assert(bootstrap.SeriesToUpload(cfg, nil), gc.DeepEquals, []string{"quantal", "precise"}) + c.Assert(bootstrap.SeriesToUpload(cfg, []string{"quantal"}), gc.DeepEquals, []string{"quantal"}) + env = newEnviron("foo", useDefaultKeys, map[string]interface{}{"default-series": "lucid"}) + cfg = env.Config() + c.Assert(bootstrap.SeriesToUpload(cfg, nil), gc.DeepEquals, []string{"quantal", "precise", "lucid"}) +} + +func (s *bootstrapSuite) assertUploadTools(c *gc.C, vers version.Binary, allowRelease bool, + extraConfig map[string]interface{}, errMessage string) { + + s.PatchValue(&version.Current, vers) + // If we allow released tools to be uploaded, the build number is incremented so in that case + // we need to ensure the environment is set up to allow dev tools to be used. + env := newEnviron("foo", useDefaultKeys, extraConfig) + s.setDummyStorage(c, env) + envtesting.RemoveFakeTools(c, env.Storage()) + + // At this point, as a result of setDummyStorage, env has tools for amd64 uploaded. + // Set version.Current to be arm64 to simulate a different CLI version. + cliVersion := version.Current + cliVersion.Arch = "arm64" + version.Current = cliVersion + s.PatchValue(&sync.BuildToolsTarball, s.getMockBuildTools(c)) + // Host runs arm64, environment supports arm64. + s.PatchValue(&arch.HostArch, func() string { + return "arm64" + }) + arch := "arm64" + err := bootstrap.UploadTools(env, &arch, allowRelease, "precise") + if errMessage != "" { + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Assert(stripped, gc.Matches, errMessage) + return + } + c.Assert(err, gc.IsNil) + params := envtools.BootstrapToolsParams{ + Arch: &arch, + Series: "precise", + } + agentTools, err := envtools.FindBootstrapTools(env, params) + c.Assert(err, gc.IsNil) + c.Assert(agentTools, gc.HasLen, 1) + expectedVers := vers + expectedVers.Number.Build++ + expectedVers.Series = "precise" + c.Assert(agentTools[0].Version, gc.DeepEquals, expectedVers) +} + +func (s *bootstrapSuite) TestUploadTools(c *gc.C) { + vers := version.MustParseBinary("1.19.0-trusty-arm64") + s.assertUploadTools(c, vers, false, nil, "") +} + +func (s *bootstrapSuite) TestUploadToolsForceVersionAllowsReleaseTools(c *gc.C) { + vers := version.MustParseBinary("1.18.0-trusty-arm64") + extraCfg := map[string]interface{}{"development": true} + s.assertUploadTools(c, vers, true, extraCfg, "") +} + +func (s *bootstrapSuite) TestUploadToolsForceVersionAllowsAgentVersionSet(c *gc.C) { + vers := version.MustParseBinary("1.18.0-trusty-arm64") + extraCfg := map[string]interface{}{"agent-version": "1.18.0", "development": true} + s.assertUploadTools(c, vers, true, extraCfg, "") +} + +func (s *bootstrapSuite) TestUploadToolsReleaseVersionDisallowed(c *gc.C) { + vers := version.MustParseBinary("1.18.0-trusty-arm64") + s.assertUploadTools(c, vers, false, nil, "Juju cannot bootstrap because no tools are available for your environment.*") } type bootstrapEnviron struct { @@ -218,11 +410,12 @@ // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. func (e *bootstrapEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. - return []simplestreams.DataSource{storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil + return []simplestreams.DataSource{ + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil } -func newEnviron(name string, defaultKeys bool) *bootstrapEnviron { - m := dummy.SampleConfig() +func newEnviron(name string, defaultKeys bool, extraAttrs map[string]interface{}) *bootstrapEnviron { + m := dummy.SampleConfig().Merge(extraAttrs) if !defaultKeys { m = m.Delete( "ca-cert", @@ -272,3 +465,7 @@ func (e *bootstrapEnviron) Storage() storage.Storage { return e.storage } + +func (e *bootstrapEnviron) SupportedArchitectures() ([]string, error) { + return []string{"amd64", "arm64"}, nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/state.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/state.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state.go 2014-03-20 12:52:38.000000000 +0000 @@ -40,6 +40,7 @@ // putState writes the given data to the state file on the given storage. // The file's name is as defined in StateFile. func putState(storage storage.StorageWriter, data []byte) error { + logger.Debugf("putting %q to bootstrap storage %T", StateFile, storage) return storage.Put(StateFile, bytes.NewBuffer(data), int64(len(data))) } @@ -69,6 +70,7 @@ // LoadStateFromURL reads state from the given URL. func LoadStateFromURL(url string, disableSSLHostnameVerification bool) (*BootstrapState, error) { + logger.Debugf("loading %q from %q", StateFile, url) client := http.DefaultClient if disableSSLHostnameVerification { logger.Infof("hostname SSL verification disabled") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/state_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/state_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,7 @@ "os" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -18,7 +19,6 @@ "launchpad.net/juju-core/environs/storage" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/instance" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/synctools.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/synctools.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/bootstrap/synctools.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/synctools.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,9 +8,11 @@ "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/sync" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/juju/arch" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils/set" "launchpad.net/juju-core/version" @@ -21,46 +23,31 @@ ` const noToolsNoUploadMessage = `Juju cannot bootstrap because no tools are available for your environment. -In addition, no tools could be located to upload. You may want to use the 'tools-metadata-url' configuration setting to specify the tools location. ` -// syncOrUpload first attempts to synchronize tools from -// the default tools source to the environment's storage. -// -// If synchronization fails due to no matching tools, -// a development version of juju is running, and no -// agent-version has been specified, then attempt to -// build and upload local tools. -func syncOrUpload(env environs.Environ, bootstrapSeries string) error { - sctx := &sync.SyncContext{ - Target: env.Storage(), - } - err := sync.SyncTools(sctx) - if err == coretools.ErrNoMatches || err == envtools.ErrNoTools { - if _, hasAgentVersion := env.Config().AgentVersion(); !hasAgentVersion && version.Current.IsDev() { - logger.Warningf("no tools found, so attempting to build and upload new tools") - if err = uploadTools(env, bootstrapSeries); err != nil { - logger.Errorf("%s", noToolsMessage) - return err - } - } else { - logger.Errorf("%s", noToolsNoUploadMessage) +// UploadTools uploads tools for the specified series and any other relevant series to +// the environment storage, after which it sets the agent-version. If forceVersion is true, +// we allow uploading release tools versions and allow uploading even when the agent-version is +// already set in the environment. +func UploadTools(env environs.Environ, toolsArch *string, forceVersion bool, bootstrapSeries ...string) error { + logger.Infof("checking that upload is possible") + // Check the series are valid. + for _, series := range bootstrapSeries { + if _, err := simplestreams.SeriesVersion(series); err != nil { + return err } } - return err -} + // See that we are allowed to upload the tools. + if err := validateUploadAllowed(env, toolsArch, forceVersion); err != nil { + return err + } -func uploadTools(env environs.Environ, bootstrapSeries string) error { cfg := env.Config() - uploadVersion := version.Current.Number - uploadVersion.Build++ - uploadSeries := set.NewStrings( - bootstrapSeries, - cfg.DefaultSeries(), - config.DefaultSeries, - ) - tools, err := sync.Upload(env.Storage(), &uploadVersion, uploadSeries.Values()...) + explicitVersion := uploadVersion(version.Current.Number, nil) + uploadSeries := SeriesToUpload(cfg, bootstrapSeries) + logger.Infof("uploading tools for series %s", uploadSeries) + tools, err := sync.Upload(env.Storage(), &explicitVersion, uploadSeries...) if err != nil { return err } @@ -76,9 +63,75 @@ return nil } +// uploadVersion returns a copy of the supplied version with a build number +// higher than any of the supplied tools that share its major, minor and patch. +func uploadVersion(vers version.Number, existing coretools.List) version.Number { + vers.Build++ + for _, t := range existing { + if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { + continue + } + if t.Version.Build >= vers.Build { + vers.Build = t.Version.Build + 1 + } + } + return vers +} + +// SeriesToUpload returns the supplied series with duplicates removed if +// non-empty; otherwise it returns a default list of series we should +// probably upload, based on cfg. +func SeriesToUpload(cfg *config.Config, series []string) []string { + unique := set.NewStrings(series...) + if unique.IsEmpty() { + unique.Add(version.Current.Series) + unique.Add(config.DefaultSeries) + unique.Add(cfg.DefaultSeries()) + } + return unique.Values() +} + +// validateUploadAllowed returns an error if an attempt to upload tools should +// not be allowed. +func validateUploadAllowed(env environs.Environ, toolsArch *string, forceVersion bool) error { + if !forceVersion { + // First, check that there isn't already an agent version specified, and that we + // are running a development version. + if _, hasAgentVersion := env.Config().AgentVersion(); hasAgentVersion || !version.Current.IsDev() { + return fmt.Errorf(noToolsNoUploadMessage) + } + } + // Now check that the architecture for which we are setting up an + // environment matches that from which we are bootstrapping. + hostArch := arch.HostArch() + // We can't build tools for a different architecture if one is specified. + if toolsArch != nil && *toolsArch != hostArch { + return fmt.Errorf("cannot build tools for %q using a machine running on %q", *toolsArch, hostArch) + } + // If no architecture is specified, ensure the target provider supports instances matching our architecture. + supportedArchitectures, err := env.SupportedArchitectures() + if err != nil { + return fmt.Errorf( + "no packaged tools available and cannot determine environment's supported architectures: %v", err) + } + archSupported := false + for _, arch := range supportedArchitectures { + if hostArch == arch { + archSupported = true + break + } + } + if !archSupported { + envType := env.Config().Type() + return fmt.Errorf( + "environment %q of type %s does not support instances running on %q", env.Name(), envType, hostArch) + } + return nil +} + // EnsureToolsAvailability verifies the tools are available. If no tools are // found, it will automatically synchronize them. -func EnsureToolsAvailability(env environs.Environ, series string, arch *string) (coretools.List, error) { +func EnsureToolsAvailability(env environs.Environ, series string, toolsArch *string) (coretools.List, error) { cfg := env.Config() var vers *version.Number if agentVersion, ok := cfg.AgentVersion(); ok { @@ -87,11 +140,11 @@ logger.Debugf( "looking for bootstrap tools: series=%q, arch=%v, version=%v", - series, arch, vers, + series, toolsArch, vers, ) params := envtools.BootstrapToolsParams{ Version: vers, - Arch: arch, + Arch: toolsArch, Series: series, // If vers.Build>0, the tools may have been uploaded in this session. // Allow retries, so we wait until the storage has caught up. @@ -104,17 +157,14 @@ return nil, err } - // No tools available, so synchronize. - logger.Warningf("no tools available, attempting to retrieve from %v", envtools.DefaultBaseURL) - if syncErr := syncOrUpload(env, series); syncErr != nil { - // The target may have tools that don't match, so don't - // return a misleading "no tools found" error. - if syncErr != envtools.ErrNoTools { - err = syncErr - } - return nil, fmt.Errorf("cannot find bootstrap tools: %v", err) + // No tools available so our only hope is to build locally and upload. + logger.Warningf("no prepackaged tools available") + uploadSeries := SeriesToUpload(cfg, nil) + if err := UploadTools(env, toolsArch, false, append(uploadSeries, series)...); err != nil { + logger.Errorf("%s", noToolsMessage) + return nil, fmt.Errorf("cannot upload bootstrap tools: %v", err) } - // TODO(axw) have syncOrUpload return the list of tools in the target, and use that. + // TODO(axw) have uploadTools return the list of tools in the target, and use that. params.AllowRetry = true if toolsList, err = envtools.FindBootstrapTools(env, params); err != nil { return nil, fmt.Errorf("cannot find bootstrap tools: %v", err) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/broker.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/broker.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/broker.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/broker.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,32 @@ "launchpad.net/juju-core/tools" ) +// Networks holds network include/exclude +// configuration +type Networks struct { + IncludedNetworks []string + ExcludedNetworks []string +} + +// StartInstanceParams holds parameters for the +// InstanceBroker.StartInstace method. +type StartInstanceParams struct { + // Constraints is a set of constraints on + // the kind of instance to create. + Constraints constraints.Value + + // Networks holds 2 string slices indicating + // respectively included and excluded Networks + Networks Networks + + // Tools is a list of tools that may be used + // to start a Juju agent on the machine. + Tools tools.List + + // MachineConfig describes the machine's configuration. + MachineConfig *cloudinit.MachineConfig +} + // TODO(wallyworld) - we want this in the environs/instance package but import loops // stop that from being possible right now. type InstanceBroker interface { @@ -19,10 +45,7 @@ // unique within an environment, is used by juju to protect against the // consequences of multiple instances being started with the same machine // id. - StartInstance( - cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig, - ) (instance.Instance, *instance.HardwareCharacteristics, error) + StartInstance(args StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) // StopInstances shuts down the given instances. StopInstances([]instance.Instance) error diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,6 +14,7 @@ "launchpad.net/goyaml" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/agent/mongo" agenttools "launchpad.net/juju-core/agent/tools" "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/constraints" @@ -23,6 +24,7 @@ "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/upstart" "launchpad.net/juju-core/utils" @@ -91,6 +93,9 @@ // LogDir holds the directory that juju logs will be written to. LogDir string + // Jobs holds what machine jobs to run. + Jobs []params.MachineJob + // CloudInitOutputLog specifies the path to the output log for cloud-init. // The directory containing the log file must already exist. CloudInitOutputLog string @@ -206,6 +211,31 @@ return nil } +// AddAptCommands update the cloudinit.Config instance with the necessary +// packages, the request to do the apt-get update/upgrade on boot, and adds +// the apt proxy settings if there are any. +func AddAptCommands(proxy osenv.ProxySettings, c *cloudinit.Config) { + // Bring packages up-to-date. + c.SetAptUpdate(true) + c.SetAptUpgrade(true) + + // juju requires git for managing charm directories. + c.AddPackage("git") + c.AddPackage("cpu-checker") + c.AddPackage("bridge-utils") + c.AddPackage("rsyslog-gnutls") + + // Write out the apt proxy settings + if (proxy != osenv.ProxySettings{}) { + filename := utils.AptConfFile + c.AddBootCmd(fmt.Sprintf( + `[ -f %s ] || (printf '%%s\n' %s > %s)`, + filename, + shquote(utils.AptProxyContent(proxy)), + filename)) + } +} + // ConfigureJuju updates the provided cloudinit.Config with configuration // to initialise a Juju machine agent. func ConfigureJuju(cfg *MachineConfig, c *cloudinit.Config) error { @@ -230,25 +260,7 @@ } if !cfg.DisablePackageCommands { - // Bring packages up-to-date. - c.SetAptUpdate(true) - c.SetAptUpgrade(true) - - // juju requires git for managing charm directories. - c.AddPackage("git") - c.AddPackage("cpu-checker") - c.AddPackage("bridge-utils") - c.AddPackage("rsyslog-gnutls") - - // Write out the apt proxy settings - if (cfg.AptProxySettings != osenv.ProxySettings{}) { - filename := utils.AptConfFile - c.AddBootCmd(fmt.Sprintf( - `[ -f %s ] || (printf '%%s\n' %s > %s)`, - filename, - shquote(utils.AptProxyContent(cfg.AptProxySettings)), - filename)) - } + AddAptCommands(cfg.AptProxySettings, c) } // Write out the normal proxy settings so that the settings are @@ -287,9 +299,9 @@ if strings.HasPrefix(cfg.Tools.URL, fileSchemePrefix) { copyCmd = fmt.Sprintf("cp %s $bin/tools.tar.gz", shquote(cfg.Tools.URL[len(fileSchemePrefix):])) } else { - curlCommand := "curl" + curlCommand := "curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s '" if cfg.DisableSSLHostnameVerification { - curlCommand = "curl --insecure" + curlCommand += " --insecure" } copyCmd = fmt.Sprintf("%s -o $bin/tools.tar.gz %s", curlCommand, shquote(cfg.Tools.URL)) c.AddRunCmd(cloudinit.LogProgressCmd("Fetching tools: %s", copyCmd)) @@ -326,32 +338,39 @@ // for series that need it. This gives us up-to-date LXC, // MongoDB, and other infrastructure. if !cfg.DisablePackageCommands { - cfg.MaybeAddCloudArchiveCloudTools(c) + series := cfg.Tools.Version.Series + MaybeAddCloudArchiveCloudTools(c, series) } if cfg.StateServer { identityFile := cfg.dataFile(SystemIdentity) c.AddFile(identityFile, cfg.SystemPrivateSSHKey, 0600) if !cfg.DisablePackageCommands { - // Disable the default mongodb installed by the mongodb-server package. - // Only do this if the file doesn't exist already, so users can run - // their own mongodb server if they wish to. - c.AddBootCmd( - `[ -f /etc/default/mongodb ] || + series := cfg.Tools.Version.Series + mongoPackage := mongo.MongoPackageForSeries(series) + if mongoPackage == "mongodb-server" { + // Disable the default mongodb installed by the mongodb-server package. + // Only do this if the file doesn't exist already, so users can run + // their own mongodb server if they wish to. + c.AddBootCmd( + `[ -f /etc/default/mongodb ] || (echo ENABLE_MONGODB="no" > /etc/default/mongodb)`) - if cfg.NeedMongoPPA() { - const key = "" // key is loaded from PPA - c.AddAptSource("ppa:juju/stable", key, nil) - } - if cfg.Tools.Version.Series == "precise" { - // In precise we add the cloud-tools pocket and - // pin it with a lower priority, so we need to - // explicitly specify the target release when - // installing mongodb-server from there. - c.AddPackageFromTargetRelease("mongodb-server", "precise-updates/cloud-tools") + if cfg.NeedMongoPPA() { + const key = "" // key is loaded from PPA + c.AddAptSource("ppa:juju/stable", key, nil) + } + if series == "precise" { + // In precise we add the cloud-tools pocket and + // pin it with a lower priority, so we need to + // explicitly specify the target release when + // installing mongodb-server from there. + c.AddPackageFromTargetRelease("mongodb-server", "precise-updates/cloud-tools") + } else { + c.AddPackage("mongodb-server") + } } else { - c.AddPackage("mongodb-server") + c.AddPackage(mongoPackage) } } certKey := string(cfg.StateServerCert) + string(cfg.StateServerKey) @@ -406,6 +425,8 @@ } configParams := agent.AgentConfigParams{ DataDir: cfg.DataDir, + LogDir: cfg.LogDir, + Jobs: cfg.Jobs, Tag: tag, UpgradedToVersion: version.Current.Number, Password: password, @@ -477,7 +498,11 @@ ) name := cfg.MongoServiceName - conf := upstart.MongoUpstartService(name, cfg.DataDir, dbDir, cfg.StatePort) + mongodExec := mongo.MongodPathForSeries(cfg.Tools.Version.Series) + conf, err := mongo.MongoUpstartService(name, mongodExec, cfg.DataDir, cfg.StatePort) + if err != nil { + return err + } cmds, err := conf.InstallCommands() if err != nil { return errgo.Annotate(err, "cannot make cloud-init upstart script for the state database") @@ -573,8 +598,7 @@ // MaybeAddCloudArchiveCloudTools adds the cloud-archive cloud-tools // pocket to apt sources, if the series requires it. -func (cfg *MachineConfig) MaybeAddCloudArchiveCloudTools(c *cloudinit.Config) { - series := cfg.Tools.Version.Series +func MaybeAddCloudArchiveCloudTools(c *cloudinit.Config, series string) { if series != "precise" { // Currently only precise; presumably we'll // need to add each LTS in here as they're @@ -622,6 +646,9 @@ if cfg.LogDir == "" { return fmt.Errorf("missing log directory") } + if len(cfg.Jobs) == 0 { + return fmt.Errorf("missing machine jobs") + } if cfg.CloudInitOutputLog == "" { return fmt.Errorf("missing cloud-init output log path") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "regexp" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -21,11 +22,10 @@ "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" - "launchpad.net/juju-core/upstart" "launchpad.net/juju-core/version" ) @@ -39,6 +39,13 @@ var envConstraints = constraints.MustParse("mem=2G") +var allMachineJobs = []params.MachineJob{ + params.JobManageEnviron, params.JobHostUnits, +} +var normalMachineJobs = []params.MachineJob{ + params.JobHostUnits, +} + type cloudinitTest struct { cfg cloudinit.MachineConfig setEnvConfig bool @@ -57,8 +64,12 @@ return cfg } -// mongodPath is the path where we should expect mongod in the environment. -var mongodPath = upstart.MongodPath() +func must(s string, err error) string { + if err != nil { + panic(err) + } + return s +} // Each test gives a cloudinit config - we check the // output to see if it looks correct. @@ -87,7 +98,8 @@ }, Constraints: envConstraints, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateInfoURL: "some-url", SystemPrivateSSHKey: "private rsa key", @@ -108,15 +120,13 @@ echo 'Fetching tools.* bin='/var/lib/juju/tools/1\.2\.3-precise-amd64' mkdir -p \$bin -curl -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz' +curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz' sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-precise-amd64\.sha256 grep '1234' \$bin/juju1\.2\.3-precise-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\) tar zxf \$bin/tools.tar.gz -C \$bin rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-precise-amd64\.sha256 printf %s '{"version":"1\.2\.3-precise-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt mkdir -p '/var/lib/juju/agents/machine-0' -install -m 644 /dev/null '/var/lib/juju/agents/machine-0/format' -printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/format' install -m 600 /dev/null '/var/lib/juju/agents/machine-0/agent\.conf' printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/agent\.conf' install -D -m 644 /dev/null '/etc/apt/preferences\.d/50-cloud-tools' @@ -131,11 +141,9 @@ dd bs=1M count=1 if=/dev/zero of=/var/lib/juju/db/journal/prealloc\.1 dd bs=1M count=1 if=/dev/zero of=/var/lib/juju/db/journal/prealloc\.2 echo 'Starting MongoDB server \(juju-db\)'.* -cat >> /etc/init/juju-db\.conf << 'EOF'\\ndescription "juju state database"\\nauthor "Juju Team "\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 65000 65000\\nlimit nproc 20000 20000\\n\\nexec ` + mongodPath + ` --auth --dbpath=/var/lib/juju/db --sslOnNormalPorts --sslPEMKeyFile '/var/lib/juju/server\.pem' --sslPEMKeyPassword ignored --bind_ip 0\.0\.0\.0 --port 37017 --noprealloc --syslog --smallfiles\\nEOF\\n +cat >> /etc/init/juju-db\.conf << 'EOF'\\ndescription "juju state database"\\nauthor "Juju Team "\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 65000 65000\\nlimit nproc 20000 20000\\n\\nexec /usr/bin/mongod --auth --dbpath=/var/lib/juju/db --sslOnNormalPorts --sslPEMKeyFile '/var/lib/juju/server\.pem' --sslPEMKeyPassword ignored --bind_ip 0\.0\.0\.0 --port 37017 --noprealloc --syslog --smallfiles\\nEOF\\n start juju-db mkdir -p '/var/lib/juju/agents/bootstrap' -install -m 644 /dev/null '/var/lib/juju/agents/bootstrap/format' -printf '%s\\n' '.*' > '/var/lib/juju/agents/bootstrap/format' install -m 600 /dev/null '/var/lib/juju/agents/bootstrap/agent\.conf' printf '%s\\n' '.*' > '/var/lib/juju/agents/bootstrap/agent\.conf' echo 'Bootstrapping Juju machine agent'.* @@ -171,7 +179,8 @@ }, Constraints: envConstraints, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateInfoURL: "some-url", SystemPrivateSSHKey: "private rsa key", @@ -182,7 +191,7 @@ inexactMatch: true, expectScripts: ` bin='/var/lib/juju/tools/1\.2\.3-raring-amd64' -curl -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-raring-amd64\.tgz' +curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-raring-amd64\.tgz' sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-raring-amd64\.sha256 grep '1234' \$bin/juju1\.2\.3-raring-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\) rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-raring-amd64\.sha256 @@ -192,13 +201,52 @@ ln -s 1\.2\.3-raring-amd64 '/var/lib/juju/tools/machine-0' `, }, { + // trusty state server - use the new mongo from juju-mongodb + cfg: cloudinit.MachineConfig{ + MachineId: "0", + AuthorizedKeys: "sshkey1", + AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, + // raring provides mongo in the archive + Tools: newSimpleTools("1.2.3-trusty-amd64"), + StateServer: true, + StateServerCert: serverCert, + StateServerKey: serverKey, + StatePort: 37017, + APIPort: 17070, + MachineNonce: "FAKE_NONCE", + StateInfo: &state.Info{ + Password: "arble", + CACert: []byte("CA CERT\n" + testing.CACert), + }, + APIInfo: &api.Info{ + Password: "bletch", + CACert: []byte("CA CERT\n" + testing.CACert), + }, + Constraints: envConstraints, + DataDir: environs.DataDir, + LogDir: agent.DefaultLogDir, + Jobs: allMachineJobs, + CloudInitOutputLog: environs.CloudInitOutputLog, + StateInfoURL: "some-url", + SystemPrivateSSHKey: "private rsa key", + MachineAgentServiceName: "jujud-machine-0", + MongoServiceName: "juju-db", + }, + setEnvConfig: true, + inexactMatch: true, + expectScripts: ` +echo 'Starting MongoDB server \(juju-db\)'.* +cat >> /etc/init/juju-db\.conf << 'EOF'\\ndescription "juju state database"\\nauthor "Juju Team "\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 65000 65000\\nlimit nproc 20000 20000\\n\\nexec /usr/lib/juju/bin/mongod --auth --dbpath=/var/lib/juju/db --sslOnNormalPorts --sslPEMKeyFile '/var/lib/juju/server\.pem' --sslPEMKeyPassword ignored --bind_ip 0\.0\.0\.0 --port 37017 --noprealloc --syslog --smallfiles\\nEOF\\n +`, + }, { // non state server. cfg: cloudinit.MachineConfig{ MachineId: "99", AuthorizedKeys: "sshkey1", AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: normalMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateServer: false, Tools: newSimpleTools("1.2.3-linux-amd64"), @@ -229,15 +277,13 @@ echo 'Fetching tools.* bin='/var/lib/juju/tools/1\.2\.3-linux-amd64' mkdir -p \$bin -curl -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz' +curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz' sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-linux-amd64\.sha256 grep '1234' \$bin/juju1\.2\.3-linux-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\) tar zxf \$bin/tools.tar.gz -C \$bin rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256 printf %s '{"version":"1\.2\.3-linux-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt mkdir -p '/var/lib/juju/agents/machine-99' -install -m 644 /dev/null '/var/lib/juju/agents/machine-99/format' -printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/format' install -m 600 /dev/null '/var/lib/juju/agents/machine-99/agent\.conf' printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/agent\.conf' ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-99' @@ -253,7 +299,8 @@ AuthorizedKeys: "sshkey1", AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: normalMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateServer: false, Tools: newSimpleTools("1.2.3-linux-amd64"), @@ -275,8 +322,6 @@ inexactMatch: true, expectScripts: ` mkdir -p '/var/lib/juju/agents/machine-2-lxc-1' -install -m 644 /dev/null '/var/lib/juju/agents/machine-2-lxc-1/format' -printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-2-lxc-1/format' install -m 600 /dev/null '/var/lib/juju/agents/machine-2-lxc-1/agent\.conf' printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-2-lxc-1/agent\.conf' ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-2-lxc-1' @@ -290,7 +335,8 @@ AuthorizedKeys: "sshkey1", AgentEnvironment: map[string]string{agent.ProviderType: "dummy"}, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: normalMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateServer: false, Tools: newSimpleTools("1.2.3-linux-amd64"), @@ -312,7 +358,7 @@ }, inexactMatch: true, expectScripts: ` -curl --insecure -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz' +curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' --insecure -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz' `, }, { // empty contraints. @@ -337,7 +383,8 @@ CACert: []byte("CA CERT\n" + testing.CACert), }, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, StateInfoURL: "some-url", SystemPrivateSSHKey: "private rsa key", @@ -368,7 +415,7 @@ } func getAgentConfig(c *gc.C, tag string, scripts []string) (cfg string) { - re := regexp.MustCompile(`printf '%s\\n' '([^']+)' > .*agents/` + regexp.QuoteMeta(tag) + `/agent\.conf`) + re := regexp.MustCompile(`printf '%s\\n' '((\n|.)+)' > .*agents/` + regexp.QuoteMeta(tag) + `/agent\.conf`) found := false for _, s := range scripts { m := re.FindStringSubmatch(s) @@ -405,6 +452,12 @@ // TestCloudInit checks that the output from the various tests // in cloudinitTests is well formed. func (*cloudinitSuite) TestCloudInit(c *gc.C) { + expectedMongoPackage := map[string]string{ + "precise": "mongodb-server", + "raring": "mongodb-server", + "trusty": "juju-mongodb", + } + for i, test := range cloudinitTests { c.Logf("test %d", i) if test.setEnvConfig { @@ -438,7 +491,9 @@ acfg := getAgentConfig(c, tag, scripts) c.Assert(acfg, jc.Contains, "AGENT_SERVICE_NAME: jujud-"+tag) if test.cfg.StateServer { - checkPackage(c, x, "mongodb-server", true) + series := test.cfg.Tools.Version.Series + mongoPackage := expectedMongoPackage[series] + checkPackage(c, x, mongoPackage, true) source := "ppa:juju/stable" checkAptSource(c, x, source, "", test.cfg.NeedMongoPPA()) c.Assert(acfg, jc.Contains, "MONGO_SERVICE_NAME: juju-db") @@ -754,7 +809,8 @@ }, Config: minimalConfig(c), DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: normalMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, MachineNonce: "FAKE_NONCE", SystemPrivateSSHKey: "private rsa key", @@ -821,6 +877,7 @@ environConfig := minimalConfig(c) environConfig, err := environConfig.Apply(map[string]interface{}{ "http-proxy": "http://user@10.0.0.1", + "no-proxy": "localhost,10.0.3.1", }) c.Assert(err, gc.IsNil) machineCfg := s.createMachineConfig(c, environConfig) @@ -833,13 +890,17 @@ expected := []interface{}{ `export http_proxy=http://user@10.0.0.1`, `export HTTP_PROXY=http://user@10.0.0.1`, + `export no_proxy=localhost,10.0.3.1`, + `export NO_PROXY=localhost,10.0.3.1`, `[ -e /home/ubuntu ] && (printf '%s\n' 'export http_proxy=http://user@10.0.0.1 -export HTTP_PROXY=http://user@10.0.0.1' > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`, +export HTTP_PROXY=http://user@10.0.0.1 +export no_proxy=localhost,10.0.3.1 +export NO_PROXY=localhost,10.0.3.1' > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`, } found := false for i, cmd := range cmds { if cmd == first { - c.Assert(cmds[i+1:i+4], jc.DeepEquals, expected) + c.Assert(cmds[i+1:i+6], jc.DeepEquals, expected) found = true break } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit.go 2014-03-20 12:52:38.000000000 +0000 @@ -17,16 +17,14 @@ "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/utils" ) // DataDir is the default data directory. // Tests can override this where needed, so they don't need to mess with global // system state. -var DataDir = "/var/lib/juju" - -// LogDir is the default log file path. -const LogDir = "/var/log/juju" +var DataDir = agent.DefaultDataDir // CloudInitOutputLog is the default cloud-init-output.log file path. const CloudInitOutputLog = "/var/log/cloud-init-output.log" @@ -42,7 +40,8 @@ return &cloudinit.MachineConfig{ // Fixed entries. DataDir: DataDir, - LogDir: LogDir, + LogDir: agent.DefaultLogDir, + Jobs: []params.MachineJob{params.JobHostUnits}, CloudInitOutputLog: CloudInitOutputLog, MachineAgentServiceName: "jujud-" + names.MachineTag(machineID), MongoServiceName: MongoServiceName, @@ -66,6 +65,7 @@ mcfg.StateServer = true mcfg.StateInfoURL = stateInfoURL mcfg.SystemPrivateSSHKey = privateSystemSSHKey + mcfg.Jobs = []params.MachineJob{params.JobManageEnviron, params.JobHostUnits} return mcfg } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/cloudinit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -19,8 +20,8 @@ "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" @@ -154,6 +155,10 @@ envConfig, err := config.New(config.NoDefaults, dummySampleConfig()) c.Assert(err, gc.IsNil) + allJobs := []params.MachineJob{ + params.JobManageEnviron, + params.JobHostUnits, + } cfg := &cloudinit.MachineConfig{ MachineId: "10", MachineNonce: "5432", @@ -173,7 +178,8 @@ Tag: "machine-10", }, DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: allJobs, CloudInitOutputLog: environs.CloudInitOutputLog, Config: envConfig, StatePort: envConfig.StatePort(), diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/config/config.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/config/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/config/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/config/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,7 @@ "time" "github.com/errgo/errgo" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cert" "launchpad.net/juju-core/charm" @@ -442,12 +442,14 @@ return c.mustString("authorized-keys") } -// ProxySettings returns all three proxy settings; http, https and ftp. +// ProxySettings returns all four proxy settings; http, https, ftp, and no +// proxy. func (c *Config) ProxySettings() osenv.ProxySettings { return osenv.ProxySettings{ - Http: c.HttpProxy(), - Https: c.HttpsProxy(), - Ftp: c.FtpProxy(), + Http: c.HttpProxy(), + Https: c.HttpsProxy(), + Ftp: c.FtpProxy(), + NoProxy: c.NoProxy(), } } @@ -466,6 +468,11 @@ return c.asString("ftp-proxy") } +// NoProxy returns the 'no proxy' for the environment. +func (c *Config) NoProxy() string { + return c.asString("no-proxy") +} + func (c *Config) getWithFallback(key, fallback string) string { value := c.asString(key) if value == "" { @@ -627,6 +634,13 @@ return "released" } +// TestMode indicates if the environment is intended for testing. +// In this case, accessing the charm store does not affect statistical +// data of the store. +func (c *Config) TestMode() bool { + return c.defined["test-mode"].(bool) +} + // UnknownAttrs returns a copy of the raw configuration attributes // that are supposedly specific to the environment type. They could // also be wrong attributes, though. Only the specific environment @@ -648,6 +662,15 @@ return allAttrs } +// Remove returns a new configuration that has the attributes of c minus attrs. +func (c *Config) Remove(attrs []string) (*Config, error) { + defined := c.AllAttrs() + for _, k := range attrs { + delete(defined, k) + } + return New(NoDefaults, defined) +} + // Apply returns a new configuration that has the attributes of c plus attrs. func (c *Config) Apply(attrs map[string]interface{}) (*Config, error) { defined := c.AllAttrs() @@ -685,12 +708,14 @@ "http-proxy": schema.String(), "https-proxy": schema.String(), "ftp-proxy": schema.String(), + "no-proxy": schema.String(), "apt-http-proxy": schema.String(), "apt-https-proxy": schema.String(), "apt-ftp-proxy": schema.String(), "bootstrap-timeout": schema.ForceInt(), "bootstrap-retry-delay": schema.ForceInt(), "bootstrap-addresses-delay": schema.ForceInt(), + "test-mode": schema.Bool(), // Deprecated fields, retain for backwards compatibility. "tools-url": schema.String(), @@ -713,16 +738,17 @@ "ca-private-key-path": schema.Omit, "logging-config": schema.Omit, "provisioner-safe-mode": schema.Omit, + "bootstrap-timeout": schema.Omit, + "bootstrap-retry-delay": schema.Omit, + "bootstrap-addresses-delay": schema.Omit, + "rsyslog-ca-cert": schema.Omit, "http-proxy": schema.Omit, "https-proxy": schema.Omit, "ftp-proxy": schema.Omit, + "no-proxy": schema.Omit, "apt-http-proxy": schema.Omit, "apt-https-proxy": schema.Omit, "apt-ftp-proxy": schema.Omit, - "bootstrap-timeout": schema.Omit, - "bootstrap-retry-delay": schema.Omit, - "bootstrap-addresses-delay": schema.Omit, - "rsyslog-ca-cert": schema.Omit, // Deprecated fields, retain for backwards compatibility. "tools-url": "", @@ -746,6 +772,7 @@ "charm-store-auth": "", // Previously image-stream could be set to an empty value "image-stream": "", + "test-mode": false, } func allowEmpty(attr string) bool { @@ -853,19 +880,23 @@ return cert.NewServer(caCert, caKey, time.Now().UTC().AddDate(10, 0, 0), noHostnames) } -type Authorizer interface { +type Specializer interface { WithAuthAttrs(string) charm.Repository + WithTestMode(testMode bool) charm.Repository } -// AuthorizeCharmRepo returns a repository with authentication added -// from the specified configuration. -func AuthorizeCharmRepo(repo charm.Repository, cfg *Config) charm.Repository { +// SpecializeCharmRepo returns a repository customized for given configuration. +// It adds authentication if necessary and sets a charm store's testMode flag. +func SpecializeCharmRepo(repo charm.Repository, cfg *Config) charm.Repository { // If a charm store auth token is set, pass it on to the charm store if auth, authSet := cfg.CharmStoreAuth(); authSet { - if CS, isCS := repo.(Authorizer); isCS { + if CS, isCS := repo.(Specializer); isCS { repo = CS.WithAuthAttrs(auth) } } + if CS, isCS := repo.(Specializer); isCS { + repo = CS.WithTestMode(cfg.TestMode()) + } return repo } @@ -887,22 +918,29 @@ AddressesDelay time.Duration } +func addIfNotEmpty(settings map[string]interface{}, key, value string) { + if value != "" { + settings[key] = value + } +} + // ProxyConfigMap returns a map suitable to be applied to a Config to update // proxy settings. func ProxyConfigMap(proxy osenv.ProxySettings) map[string]interface{} { - return map[string]interface{}{ - "http-proxy": proxy.Http, - "https-proxy": proxy.Https, - "ftp-proxy": proxy.Ftp, - } + settings := make(map[string]interface{}) + addIfNotEmpty(settings, "http-proxy", proxy.Http) + addIfNotEmpty(settings, "https-proxy", proxy.Https) + addIfNotEmpty(settings, "ftp-proxy", proxy.Ftp) + addIfNotEmpty(settings, "no-proxy", proxy.NoProxy) + return settings } // AptProxyConfigMap returns a map suitable to be applied to a Config to update // proxy settings. func AptProxyConfigMap(proxy osenv.ProxySettings) map[string]interface{} { - return map[string]interface{}{ - "apt-http-proxy": proxy.Http, - "apt-https-proxy": proxy.Https, - "apt-ftp-proxy": proxy.Ftp, - } + settings := make(map[string]interface{}) + addIfNotEmpty(settings, "apt-http-proxy", proxy.Http) + addIfNotEmpty(settings, "apt-https-proxy", proxy.Https) + addIfNotEmpty(settings, "apt-ftp-proxy", proxy.Ftp) + return settings } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/config/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/config/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/config/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/config/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,7 +9,8 @@ stdtesting "testing" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cert" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/schema" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -629,6 +629,14 @@ "type": "null", "name": "my-name", }, + }, { + about: "TestMode flag specified", + useDefaults: config.UseDefaults, + attrs: testing.Attrs{ + "type": "my-type", + "name": "my-name", + "test-mode": true, + }, }, authTokenConfigTest("token=value, tokensecret=value", true), authTokenConfigTest("token=value, ", true), @@ -850,6 +858,9 @@ dev, _ := test.attrs["development"].(bool) c.Assert(cfg.Development(), gc.Equals, dev) + testmode, _ := test.attrs["test-mode"].(bool) + c.Assert(cfg.TestMode(), gc.Equals, testmode) + if series, _ := test.attrs["default-series"].(string); series != "" { c.Assert(cfg.DefaultSeries(), gc.Equals, series) } else { @@ -1001,6 +1012,7 @@ "bootstrap-addresses-delay": 10, "default-series": "precise", "charm-store-auth": "token=auth", + "test-mode": false, } cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, gc.IsNil) @@ -1014,6 +1026,7 @@ attrs["tools-metadata-url"] = "" attrs["tools-url"] = "" attrs["image-stream"] = "" + // Default firewall mode is instance attrs["firewall-mode"] = string(config.FwInstance) c.Assert(cfg.AllAttrs(), jc.DeepEquals, attrs) @@ -1209,6 +1222,7 @@ "http-proxy": "http://user@10.0.0.1", "https-proxy": "https://user@10.0.0.1", "ftp-proxy": "ftp://user@10.0.0.1", + "no-proxy": "localhost,10.0.3.1", }) c.Assert(config.HttpProxy(), gc.Equals, "http://user@10.0.0.1") c.Assert(config.AptHttpProxy(), gc.Equals, "http://user@10.0.0.1") @@ -1216,6 +1230,7 @@ c.Assert(config.AptHttpsProxy(), gc.Equals, "https://user@10.0.0.1") c.Assert(config.FtpProxy(), gc.Equals, "ftp://user@10.0.0.1") c.Assert(config.AptFtpProxy(), gc.Equals, "ftp://user@10.0.0.1") + c.Assert(config.NoProxy(), gc.Equals, "localhost,10.0.3.1") } func (*ConfigSuite) TestProxyValues(c *gc.C) { @@ -1247,6 +1262,7 @@ c.Assert(config.AptHttpsProxy(), gc.Equals, "") c.Assert(config.FtpProxy(), gc.Equals, "") c.Assert(config.AptFtpProxy(), gc.Equals, "") + c.Assert(config.NoProxy(), gc.Equals, "") } func (*ConfigSuite) TestProxyConfigMap(c *gc.C) { @@ -1254,13 +1270,16 @@ cfg := newTestConfig(c, testing.Attrs{}) proxy := osenv.ProxySettings{ - Http: "http proxy", - Https: "https proxy", - Ftp: "ftp proxy", + Http: "http proxy", + Https: "https proxy", + Ftp: "ftp proxy", + NoProxy: "no proxy", } cfg, err := cfg.Apply(config.ProxyConfigMap(proxy)) c.Assert(err, gc.IsNil) c.Assert(cfg.ProxySettings(), gc.DeepEquals, proxy) + // Apt proxy and proxy differ by the content of the no-proxy values. + proxy.NoProxy = "" c.Assert(cfg.AptProxySettings(), gc.DeepEquals, proxy) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/config.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,7 +9,7 @@ "os" "path/filepath" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/goyaml" "launchpad.net/juju-core/environs/config" @@ -135,13 +135,13 @@ } // Provider returns the previously registered provider with the given type. -func Provider(typ string) (EnvironProvider, error) { - if alias, ok := providerAliases[typ]; ok { - typ = alias +func Provider(providerType string) (EnvironProvider, error) { + if alias, ok := providerAliases[providerType]; ok { + providerType = alias } - p, ok := providers[typ] + p, ok := providers[providerType] if !ok { - return nil, fmt.Errorf("no registered provider for %q", typ) + return nil, fmt.Errorf("no registered provider for %q", providerType) } return p, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/disk.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/disk.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/disk.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/disk.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "path/filepath" "github.com/errgo/errgo" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/goyaml" "launchpad.net/juju-core/errors" @@ -159,6 +159,11 @@ info.Password = creds.Password } +// Location returns the location of the environInfo in human readable format. +func (info *environInfo) Location() string { + return fmt.Sprintf("file %q", info.path) +} + // Write implements EnvironInfo.Write. func (info *environInfo) Write() error { data, err := goyaml.Marshal(info) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/disk_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/disk_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/disk_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/disk_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,11 +12,11 @@ "strings" "syscall" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/errors" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -110,6 +110,7 @@ Addresses: []string{"example.com", "kremvax.ru"}, CACert: "first line\nsecond line", }) + c.Assert(info.Location(), gc.Equals, fmt.Sprintf("file %q", dir+"/environments/someenv.jenv")) c.Assert(info.BootstrapConfig(), gc.DeepEquals, map[string]interface{}{ "secret": "blah", "arble": "bletch", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/interface.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/interface.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/interface.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/interface.go 2014-03-20 12:52:38.000000000 +0000 @@ -74,6 +74,10 @@ // associated with the environment. SetAPICredentials(APICredentials) + // Location returns the location of the source of the environment + // information in a human readable format. + Location() string + // Write writes the current information to persistent storage. // A subsequent call to ConfigStorage.ReadInfo // can retrieve it. After this call succeeds, Initialized will return true. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/interface_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/interface_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/interface_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/interface_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,10 +4,10 @@ package configstore_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/configstore" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/mem.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/mem.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/mem.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/mem.go 2014-03-20 12:52:38.000000000 +0000 @@ -71,6 +71,11 @@ return nil, errors.NotFoundf("environment %q", envName) } +// Location implements EnvironInfo.Location. +func (info *memInfo) Location() string { + return "memory" +} + // Write implements EnvironInfo.Write. func (info *memInfo) Write() error { m := info.store diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/mem_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/mem_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/configstore/mem_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/configstore/mem_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -20,3 +20,9 @@ return configstore.NewMem() } } + +func (s *memInterfaceSuite) TestMemInfoLocation(c *gc.C) { + memStore := configstore.NewMem() + memInfo, _ := memStore.CreateInfo("foo") + c.Assert(memInfo.Location(), gc.Equals, "memory") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,8 @@ "sort" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -18,7 +19,6 @@ "launchpad.net/juju-core/provider/dummy" _ "launchpad.net/juju-core/provider/manual" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/filestorage/filestorage.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/filestorage/filestorage.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/filestorage/filestorage.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/filestorage/filestorage.go 2014-03-20 12:52:38.000000000 +0000 @@ -49,6 +49,13 @@ // Get implements storage.StorageReader.Get. func (f *fileStorageReader) Get(name string) (io.ReadCloser, error) { + if isInternalPath(name) { + return nil, &os.PathError{ + Op: "Get", + Path: name, + Err: os.ErrNotExist, + } + } filename := f.fullPath(name) fi, err := os.Stat(filename) if err != nil { @@ -66,11 +73,23 @@ return file, nil } +// isInternalPath returns true if a path should be hidden from user visibility +// filestorage uses ".tmp/" as a staging directory for uploads, so we don't +// want it to be visible +func isInternalPath(path string) bool { + // This blocks both ".tmp", ".tmp/foo" but also ".tmpdir", better to be + // overly restrictive to start with + return strings.HasPrefix(path, ".tmp") +} + // List implements storage.StorageReader.List. func (f *fileStorageReader) List(prefix string) ([]string, error) { + var names []string + if isInternalPath(prefix) { + return names, nil + } prefix = filepath.Join(f.path, prefix) dir := filepath.Dir(prefix) - var names []string err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -104,48 +123,36 @@ type fileStorageWriter struct { fileStorageReader - tmpdir string } -// UseDefaultTmpDir may be passed into NewFileStorageWriter -// for the tmpdir argument, to signify that the default -// value should be used. See NewFileStorageWriter for more. -const UseDefaultTmpDir = "" - // NewFileStorageWriter returns a new read/write storag for // a directory inside the local file system. -// -// A temporary directory may be specified, in which files will be written -// to before moving to the final destination. If specified, the temporary -// directory should be on the same filesystem as the storage directory -// to ensure atomicity. If tmpdir == UseDefaultTmpDir (""), then path+".tmp" -// will be used. -// -// If tmpdir == UseDefaultTmpDir, it will be created when Put is invoked, -// and will be removed afterwards. If tmpdir != UseDefaultTmpDir, it must -// already exist, and will never be removed. -func NewFileStorageWriter(path, tmpdir string) (storage.Storage, error) { +func NewFileStorageWriter(path string) (storage.Storage, error) { reader, err := NewFileStorageReader(path) if err != nil { return nil, err } - return &fileStorageWriter{*reader.(*fileStorageReader), tmpdir}, nil + return &fileStorageWriter{*reader.(*fileStorageReader)}, nil } func (f *fileStorageWriter) Put(name string, r io.Reader, length int64) error { + if isInternalPath(name) { + return &os.PathError{ + Op: "Put", + Path: name, + Err: os.ErrPermission, + } + } fullpath := f.fullPath(name) dir := filepath.Dir(fullpath) - tmpdir := f.tmpdir - if tmpdir == UseDefaultTmpDir { - tmpdir = f.path + ".tmp" - if err := os.MkdirAll(tmpdir, 0755); err != nil && !os.IsExist(err) { - return err - } - defer os.Remove(tmpdir) + if err := os.MkdirAll(dir, 0755); err != nil { + return err } - if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) { + tmpdir := filepath.Join(f.path, ".tmp") + if err := os.MkdirAll(tmpdir, 0755); err != nil { return err } + defer os.Remove(tmpdir) // Write to a temporary file first, and then move (atomically). file, err := ioutil.TempFile(tmpdir, "juju-filestorage-") if err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/filestorage/filestorage_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/filestorage/filestorage_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/filestorage/filestorage_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/filestorage/filestorage_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,13 +15,13 @@ "strings" "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/storage" coreerrors "launchpad.net/juju-core/errors" "launchpad.net/juju-core/juju/osenv" - jc "launchpad.net/juju-core/testing/checkers" ) func TestPackage(t *testing.T) { @@ -41,7 +41,7 @@ var err error s.reader, err = filestorage.NewFileStorageReader(s.dir) c.Assert(err, gc.IsNil) - s.writer, err = filestorage.NewFileStorageWriter(s.dir, filestorage.UseDefaultTmpDir) + s.writer, err = filestorage.NewFileStorageWriter(s.dir) c.Assert(err, gc.IsNil) } @@ -83,6 +83,34 @@ } } +func (s *filestorageSuite) TestListHidesTempDir(c *gc.C) { + err := s.writer.Put("test-write", bytes.NewReader(nil), 0) + c.Assert(err, gc.IsNil) + files, err := storage.List(s.reader, "") + c.Assert(err, gc.IsNil) + c.Check(files, gc.DeepEquals, []string{"test-write"}) + files, err = storage.List(s.reader, "no-such-directory") + c.Assert(err, gc.IsNil) + c.Check(files, gc.DeepEquals, []string(nil)) + // We also pretend the .tmp directory doesn't exist. If you call a + // directory that doesn't exist, we just return an empty list of + // strings, so we force the same behavior for '.tmp' + // we poke in a file so it would have something to return + s.createFile(c, ".tmp/test-file") + files, err = storage.List(s.reader, ".tmp") + c.Assert(err, gc.IsNil) + c.Check(files, gc.DeepEquals, []string(nil)) + // For consistency, we refuse all other possibilities as well + s.createFile(c, ".tmp/foo/bar") + files, err = storage.List(s.reader, ".tmp/foo") + c.Assert(err, gc.IsNil) + c.Check(files, gc.DeepEquals, []string(nil)) + s.createFile(c, ".tmpother/foo") + files, err = storage.List(s.reader, ".tmpother") + c.Assert(err, gc.IsNil) + c.Check(files, gc.DeepEquals, []string(nil)) +} + func (s *filestorageSuite) TestURL(c *gc.C) { expectedpath, _ := s.createFile(c, "test-file") _, file := filepath.Split(expectedpath) @@ -112,6 +140,17 @@ c.Assert(err, jc.Satisfies, coreerrors.IsNotFoundError) } +func (s *filestorageSuite) TestGetRefusesTemp(c *gc.C) { + s.createFile(c, ".tmp/test-file") + _, err := storage.Get(s.reader, ".tmp/test-file") + c.Check(err, gc.NotNil) + c.Check(err, jc.Satisfies, os.IsNotExist) + s.createFile(c, ".tmp/foo/test-file") + _, err = storage.Get(s.reader, ".tmp/foo/test-file") + c.Check(err, gc.NotNil) + c.Check(err, jc.Satisfies, os.IsNotExist) +} + func (s *filestorageSuite) TestPut(c *gc.C) { data := []byte{1, 2, 3, 4, 5} err := s.writer.Put("test-write", bytes.NewReader(data), int64(len(data))) @@ -121,6 +160,20 @@ c.Assert(b, gc.DeepEquals, data) } +func (s *filestorageSuite) TestPutRefusesTmp(c *gc.C) { + data := []byte{1, 2, 3, 4, 5} + err := s.writer.Put(".tmp/test-write", bytes.NewReader(data), int64(len(data))) + c.Assert(err, gc.NotNil) + c.Check(err, jc.Satisfies, os.IsPermission) + c.Check(*err.(*os.PathError), gc.Equals, os.PathError{ + Op: "Put", + Path: ".tmp/test-write", + Err: os.ErrPermission, + }) + _, err = ioutil.ReadFile(filepath.Join(s.dir, ".tmp", "test-write")) + c.Assert(err, jc.Satisfies, os.IsNotExist) +} + func (s *filestorageSuite) TestRemove(c *gc.C) { expectedpath, _ := s.createFile(c, "test-file") _, file := filepath.Split(expectedpath) @@ -139,35 +192,21 @@ } func (s *filestorageSuite) TestPutTmpDir(c *gc.C) { - // Put should create and clean up the temporary directory if - // tmpdir==UseDefaultTmpDir. + // Put should create and clean up the temporary directory err := s.writer.Put("test-write", bytes.NewReader(nil), 0) c.Assert(err, gc.IsNil) - _, err = os.Stat(s.dir + ".tmp") + _, err = os.Stat(s.dir + "/.tmp") c.Assert(err, jc.Satisfies, os.IsNotExist) - // To deal with recovering from hard failure, UseDefaultTmpDir - // doesn't care if the temporary directory already exists. It + // To deal with recovering from hard failure, we + // don't care if the temporary directory already exists. It // still removes it, though. - err = os.Mkdir(s.dir+".tmp", 0755) + err = os.Mkdir(s.dir+"/.tmp", 0755) c.Assert(err, gc.IsNil) err = s.writer.Put("test-write", bytes.NewReader(nil), 0) c.Assert(err, gc.IsNil) - _, err = os.Stat(s.dir + ".tmp") - c.Assert(err, jc.Satisfies, os.IsNotExist) - - // If we explicitly set the temporary directory, it must already exist. - s.writer, err = filestorage.NewFileStorageWriter(s.dir, s.dir+".tmp") - c.Assert(err, gc.IsNil) - err = s.writer.Put("test-write", bytes.NewReader(nil), 0) + _, err = os.Stat(s.dir + "/.tmp") c.Assert(err, jc.Satisfies, os.IsNotExist) - err = os.Mkdir(s.dir+".tmp", 0755) - c.Assert(err, gc.IsNil) - err = s.writer.Put("test-write", bytes.NewReader(nil), 0) - c.Assert(err, gc.IsNil) - // Temporary directory should not have been moved. - _, err = os.Stat(s.dir + ".tmp") - c.Assert(err, gc.IsNil) } func (s *filestorageSuite) TestPathRelativeToHome(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/backend_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/backend_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/backend_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/backend_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,12 +16,12 @@ "strings" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/httpstorage" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" ) @@ -43,7 +43,7 @@ // a base URL for the server and the directory path. func startServer(c *gc.C) (listener net.Listener, url, dataDir string) { dataDir = c.MkDir() - embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir) + embedded, err := filestorage.NewFileStorageWriter(dataDir) c.Assert(err, gc.IsNil) listener, err = httpstorage.Serve("localhost:0", embedded) c.Assert(err, gc.IsNil) @@ -55,7 +55,7 @@ // a base URL for the server and the directory path. func startServerTLS(c *gc.C) (listener net.Listener, url, dataDir string) { dataDir = c.MkDir() - embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir) + embedded, err := filestorage.NewFileStorageWriter(dataDir) c.Assert(err, gc.IsNil) hostnames := []string{"127.0.0.1"} caCertPEM := []byte(coretesting.CACert) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/storage.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/storage.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,11 +16,15 @@ "strings" "sync" + "github.com/juju/loggo" + "launchpad.net/juju-core/environs/storage" coreerrors "launchpad.net/juju-core/errors" "launchpad.net/juju-core/utils" ) +var logger = loggo.GetLogger("juju.environs.httpstorage") + // storage implements the storage.Storage interface. type localStorage struct { addr string @@ -46,6 +50,7 @@ // using TLS. The client is given an authentication key, // which the server will verify for Put and Remove* operations. func ClientTLS(addr string, caCertPEM []byte, authkey string) (storage.Storage, error) { + logger.Debugf("using https storage at %q", addr) caCerts := x509.NewCertPool() if caCertPEM != nil { if !caCerts.AppendCertsFromPEM(caCertPEM) { @@ -83,6 +88,7 @@ // responsibility to close it after use. If the name does not // exist, it should return a *NotFoundError. func (s *localStorage) Get(name string) (io.ReadCloser, error) { + logger.Debugf("getting %q from storage", name) url, err := s.URL(name) if err != nil { return nil, err @@ -167,6 +173,7 @@ // Put reads from r and writes to the given storage file. // The length must be set to the total length of the file. func (s *localStorage) Put(name string, r io.Reader, length int64) error { + logger.Debugf("putting %q (len %d) to storage", name, length) url, err := s.modURL(name) if err != nil { return err diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/storage_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/httpstorage/storage_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,13 +11,13 @@ "net/http" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/httpstorage" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/errors" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type storageSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/generate.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/generate.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/generate.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/generate.go 2014-03-20 12:52:38.000000000 +0000 @@ -34,9 +34,9 @@ // readMetadata reads the image metadata from metadataStore. func readMetadata(metadataStore storage.Storage) ([]*ImageMetadata, error) { // Read any existing metadata so we can merge the new tools metadata with what's there. - dataSource := storage.NewStorageSimpleStreamsDataSource(metadataStore, storage.BaseImagesPath) + dataSource := storage.NewStorageSimpleStreamsDataSource("existing metadata", metadataStore, storage.BaseImagesPath) imageConstraint := NewImageConstraint(simplestreams.LookupParams{}) - existingMetadata, err := Fetch( + existingMetadata, _, err := Fetch( []simplestreams.DataSource{dataSource}, simplestreams.DefaultIndexPath, imageConstraint, false) if err != nil && !errors.IsNotFoundError(err) { return nil, err diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/generate_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/generate_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/generate_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/generate_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,8 +4,6 @@ package imagemetadata_test import ( - "sort" - gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/filestorage" @@ -28,8 +26,8 @@ Series: []string{series}, Arches: []string{arch}, }) - dataSource := storage.NewStorageSimpleStreamsDataSource(stor, "images") - metadata, err := imagemetadata.Fetch( + dataSource := storage.NewStorageSimpleStreamsDataSource("test datasource", stor, "images") + metadata, _, err := imagemetadata.Fetch( []simplestreams.DataSource{dataSource}, simplestreams.DefaultIndexPath, cons, false) c.Assert(err, gc.IsNil) c.Assert(metadata, gc.HasLen, 1) @@ -49,7 +47,7 @@ Endpoint: "endpoint", } dir := c.MkDir() - targetStorage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(dir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, targetStorage) c.Assert(err, gc.IsNil) @@ -74,7 +72,7 @@ Endpoint: "endpoint", } dir := c.MkDir() - targetStorage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(dir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) c.Assert(err, gc.IsNil) @@ -116,7 +114,7 @@ Endpoint: "endpoint", } dir := c.MkDir() - targetStorage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(dir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) c.Assert(err, gc.IsNil) @@ -141,8 +139,7 @@ im.RegionName = cloudSpec.Region im.Endpoint = cloudSpec.Endpoint } - sort.Sort(byId(metadata)) - sort.Sort(byId(newImageMetadata)) + imagemetadata.Sort(newImageMetadata) c.Assert(metadata, gc.DeepEquals, newImageMetadata) assertFetch(c, targetStorage, "raring", "amd64", "region", "endpoint", "1234") assertFetch(c, targetStorage, "precise", "amd64", "region", "endpoint", "abcd") @@ -161,7 +158,7 @@ Endpoint: "endpoint", } dir := c.MkDir() - targetStorage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(dir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", existingImageMetadata, cloudSpec, targetStorage) c.Assert(err, gc.IsNil) @@ -192,15 +189,8 @@ existingImageMetadata[0].RegionName = "region" existingImageMetadata[0].Endpoint = "endpoint" newImageMetadata = append(newImageMetadata, existingImageMetadata[0]) - sort.Sort(byId(metadata)) - sort.Sort(byId(newImageMetadata)) + imagemetadata.Sort(newImageMetadata) c.Assert(metadata, gc.DeepEquals, newImageMetadata) assertFetch(c, targetStorage, "raring", "amd64", "region", "endpoint", "1234") assertFetch(c, targetStorage, "raring", "amd64", "region2", "endpoint2", "abcd") } - -type byId []*imagemetadata.ImageMetadata - -func (b byId) Len() int { return len(b) } -func (b byId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func (b byId) Less(i, j int) bool { return b[i].Id < b[j].Id } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/marshal.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/marshal.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/marshal.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/marshal.go 2014-03-20 12:52:38.000000000 +0000 @@ -73,6 +73,7 @@ Id: t.Id, RegionName: t.RegionName, Endpoint: t.Endpoint, + VirtType: t.VirtType, } if catalog, ok := cloud.Products[t.productId()]; ok { catalog.Items[itemsversion].Items[t.Id] = toWrite diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/marshal_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/marshal_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/marshal_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/marshal_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -53,7 +53,8 @@ "19700101": { "items": { "abcd": { - "id": "abcd" + "id": "abcd", + "virt": "virt" } } } @@ -103,9 +104,10 @@ Arch: "arm", }, &imagemetadata.ImageMetadata{ - Id: "abcd", - Version: "12.04", - Arch: "amd64", + Id: "abcd", + Version: "12.04", + Arch: "amd64", + VirtType: "virt", }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,8 +8,10 @@ import ( "fmt" + "sort" "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/juju/arch" ) func init() { @@ -100,7 +102,7 @@ params.Series = simplestreams.SupportedSeries() } if len(params.Arches) == 0 { - params.Arches = []string{"amd64", "i386", "arm"} + params.Arches = arch.AllSupportedArches } return &ImageConstraint{LookupParams: params} } @@ -142,7 +144,7 @@ type ImageMetadata struct { Id string `json:"id"` Storage string `json:"root_store,omitempty"` - VType string `json:"virt,omitempty"` + VirtType string `json:"virt,omitempty"` Arch string `json:"arch,omitempty"` Version string `json:"version,omitempty"` RegionAlias string `json:"crsn,omitempty"` @@ -164,24 +166,41 @@ // The base URL locations are as specified - the first location which has a file is the one used. // Signed data is preferred, but if there is no signed data available and onlySigned is false, // then unsigned data is used. -func Fetch(sources []simplestreams.DataSource, indexPath string, cons *ImageConstraint, onlySigned bool) ([]*ImageMetadata, error) { +func Fetch( + sources []simplestreams.DataSource, indexPath string, cons *ImageConstraint, + onlySigned bool) ([]*ImageMetadata, *simplestreams.ResolveInfo, error) { params := simplestreams.ValueParams{ DataType: ImageIds, FilterFunc: appendMatchingImages, ValueTemplate: ImageMetadata{}, PublicKey: simplestreamsImagesPublicKey, } - items, err := simplestreams.GetMetadata(sources, indexPath, cons, onlySigned, params) + items, resolveInfo, err := simplestreams.GetMetadata(sources, indexPath, cons, onlySigned, params) if err != nil { - return nil, err + return nil, resolveInfo, err } metadata := make([]*ImageMetadata, len(items)) for i, md := range items { metadata[i] = md.(*ImageMetadata) } - return metadata, nil + // Sorting the metadata is not strictly necessary, but it ensures consistent ordering for + // all compilers, and it just makes it easier to look at the data. + Sort(metadata) + return metadata, resolveInfo, nil } +// Sort sorts a slice of ImageMetadata in ascending order of their id +// in order to ensure the results of Fetch are ordered deterministically. +func Sort(metadata []*ImageMetadata) { + sort.Sort(byId(metadata)) +} + +type byId []*ImageMetadata + +func (b byId) Len() int { return len(b) } +func (b byId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byId) Less(i, j int) bool { return b[i].Id < b[j].Id } + type imageKey struct { vtype string arch string @@ -198,14 +217,14 @@ imagesMap := make(map[imageKey]*ImageMetadata, len(matchingImages)) for _, val := range matchingImages { im := val.(*ImageMetadata) - imagesMap[imageKey{im.VType, im.Arch, im.Version, im.RegionName, im.Storage}] = im + imagesMap[imageKey{im.VirtType, im.Arch, im.Version, im.RegionName, im.Storage}] = im } for _, val := range images { im := val.(*ImageMetadata) if cons != nil && cons.Params().Region != "" && cons.Params().Region != im.RegionName { continue } - if _, ok := imagesMap[imageKey{im.VType, im.Arch, im.Version, im.RegionName, im.Storage}]; !ok { + if _, ok := imagesMap[imageKey{im.VirtType, im.Arch, im.Version, im.RegionName, im.Storage}]; !ok { matchingImages = append(matchingImages, im) } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -66,7 +66,8 @@ func registerSimpleStreamsTests() { gc.Suite(&simplestreamsSuite{ LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource("test:", simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource( + "test roundtripper", "test:", simplestreams.VerifySSLHostnames), RequireSigned: false, DataType: imagemetadata.ImageIds, ValidConstraint: imagemetadata.NewImageConstraint(simplestreams.LookupParams{ @@ -84,7 +85,7 @@ func registerLiveSimpleStreamsTests(baseURL string, validImageConstraint simplestreams.LookupConstraint, requireSigned bool) { gc.Suite(&sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource(baseURL, simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource("test", baseURL, simplestreams.VerifySSLHostnames), RequireSigned: requireSigned, DataType: imagemetadata.ImageIds, ValidConstraint: validImageConstraint, @@ -119,7 +120,7 @@ images: []*imagemetadata.ImageMetadata{ { Id: "ami-442ea674", - VType: "hvm", + VirtType: "hvm", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -127,7 +128,7 @@ }, { Id: "ami-442ea684", - VType: "pv", + VirtType: "pv", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -135,7 +136,7 @@ }, { Id: "ami-442ea699", - VType: "pv", + VirtType: "pv", Arch: "arm", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -150,7 +151,7 @@ images: []*imagemetadata.ImageMetadata{ { Id: "ami-442ea674", - VType: "hvm", + VirtType: "hvm", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -158,7 +159,7 @@ }, { Id: "ami-442ea684", - VType: "pv", + VirtType: "pv", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -173,7 +174,7 @@ images: []*imagemetadata.ImageMetadata{ { Id: "ami-442ea699", - VType: "pv", + VirtType: "pv", Arch: "arm", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -188,7 +189,7 @@ images: []*imagemetadata.ImageMetadata{ { Id: "ami-442ea674", - VType: "hvm", + VirtType: "hvm", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -196,7 +197,7 @@ }, { Id: "ami-442ea684", - VType: "pv", + VirtType: "pv", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -210,15 +211,23 @@ images: []*imagemetadata.ImageMetadata{ { Id: "ami-26745463", - VType: "pv", + VirtType: "pv", Arch: "amd64", RegionName: "au-east-2", Endpoint: "https://somewhere-else", Storage: "ebs", }, { + Id: "ami-26745464", + VirtType: "pv", + Arch: "amd64", + RegionName: "au-east-1", + Endpoint: "https://somewhere", + Storage: "ebs", + }, + { Id: "ami-442ea674", - VType: "hvm", + VirtType: "hvm", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -226,7 +235,7 @@ }, { Id: "ami-442ea675", - VType: "hvm", + VirtType: "hvm", Arch: "amd64", RegionAlias: "uswest3", RegionName: "us-west-3", @@ -234,16 +243,8 @@ Storage: "ebs", }, { - Id: "ami-26745464", - VType: "pv", - Arch: "amd64", - RegionName: "au-east-1", - Endpoint: "https://somewhere", - Storage: "ebs", - }, - { Id: "ami-442ea684", - VType: "pv", + VirtType: "pv", Arch: "amd64", RegionName: "us-east-1", Endpoint: "https://ec2.us-east-1.amazonaws.com", @@ -265,7 +266,11 @@ Series: []string{"precise"}, Arches: t.arches, }) - images, err := imagemetadata.Fetch([]simplestreams.DataSource{s.Source}, simplestreams.DefaultIndexPath, imageConstraint, s.RequireSigned) + // Add invalid datasource and check later that resolveInfo is correct. + invalidSource := simplestreams.NewURLDataSource("invalid", "file://invalid", simplestreams.VerifySSLHostnames) + images, resolveInfo, err := imagemetadata.Fetch( + []simplestreams.DataSource{invalidSource, s.Source}, simplestreams.DefaultIndexPath, + imageConstraint, s.RequireSigned) if !c.Check(err, gc.IsNil) { continue } @@ -273,6 +278,12 @@ testImage.Version = t.version } c.Check(images, gc.DeepEquals, t.images) + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test roundtripper", + Signed: s.RequireSigned, + IndexURL: "test:/streams/v1/index.json", + MirrorURL: "", + }) } } @@ -364,28 +375,34 @@ } func (s *signedSuite) TestSignedImageMetadata(c *gc.C) { - signedSource := simplestreams.NewURLDataSource("signedtest://host/signed", simplestreams.VerifySSLHostnames) + signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", simplestreams.VerifySSLHostnames) imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, Series: []string{"precise"}, Arches: []string{"amd64"}, }) - images, err := imagemetadata.Fetch( + images, resolveInfo, err := imagemetadata.Fetch( []simplestreams.DataSource{signedSource}, simplestreams.DefaultIndexPath, imageConstraint, true) c.Assert(err, gc.IsNil) c.Assert(len(images), gc.Equals, 1) c.Assert(images[0].Id, gc.Equals, "ami-123456") + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test", + Signed: true, + IndexURL: "signedtest://host/signed/streams/v1/index.sjson", + MirrorURL: "", + }) } func (s *signedSuite) TestSignedImageMetadataInvalidSignature(c *gc.C) { - signedSource := simplestreams.NewURLDataSource("signedtest://host/signed", simplestreams.VerifySSLHostnames) + signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", simplestreams.VerifySSLHostnames) imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, Series: []string{"precise"}, Arches: []string{"amd64"}, }) imagemetadata.SetSigningPublicKey(s.origKey) - _, err := imagemetadata.Fetch( + _, _, err := imagemetadata.Fetch( []simplestreams.DataSource{signedSource}, simplestreams.DefaultIndexPath, imageConstraint, true) c.Assert(err, gc.ErrorMatches, "cannot read index data.*") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/testing/testing.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/testing/testing.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/testing/testing.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/testing/testing.go 2014-03-20 12:52:38.000000000 +0000 @@ -27,7 +27,7 @@ // ParseMetadataFromStorage loads ImageMetadata from the specified storage reader. func ParseMetadataFromStorage(c *gc.C, stor storage.StorageReader) []*imagemetadata.ImageMetadata { - source := storage.NewStorageSimpleStreamsDataSource(stor, "images") + source := storage.NewStorageSimpleStreamsDataSource("test storage reader", stor, "images") // Find the simplestreams index file. params := simplestreams.ValueParams{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/upload.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/upload.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/upload.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/upload.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,7 +11,7 @@ "path/filepath" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/upload_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/upload_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/upload_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/upload_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "path/filepath" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/filestorage" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/environs/imagemetadata/testing" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -29,7 +29,7 @@ func createImageMetadata(c *gc.C) (sourceDir string, destDir string, destStor storage.Storage, metadata *imagemetadata.ImageMetadata) { destDir = c.MkDir() var err error - destStor, err = filestorage.NewFileStorageWriter(destDir, filestorage.UseDefaultTmpDir) + destStor, err = filestorage.NewFileStorageWriter(destDir) c.Assert(err, gc.IsNil) // Generate some metadata. @@ -48,7 +48,7 @@ im[0].RegionName = cloudSpec.Region im[0].Endpoint = cloudSpec.Endpoint var sourceStor storage.Storage - sourceStor, err = filestorage.NewFileStorageWriter(sourceDir, filestorage.UseDefaultTmpDir) + sourceStor, err = filestorage.NewFileStorageWriter(sourceDir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, sourceStor) c.Assert(err, gc.IsNil) @@ -69,7 +69,7 @@ func (s *uploadSuite) TestUploadErrors(c *gc.C) { destDir := c.MkDir() - destStor, err := filestorage.NewFileStorageWriter(destDir, filestorage.UseDefaultTmpDir) + destStor, err := filestorage.NewFileStorageWriter(destDir) c.Assert(err, gc.IsNil) err = imagemetadata.UploadImageMetadata(destStor, "foo") c.Assert(err, jc.Satisfies, os.IsNotExist) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/urls.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/urls.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/urls.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/urls.go 2014-03-20 12:52:38.000000000 +0000 @@ -31,7 +31,7 @@ if !config.SSLHostnameVerification() { verify = simplestreams.NoVerifySSLHostnames } - sources = append(sources, simplestreams.NewURLDataSource(userURL, verify)) + sources = append(sources, simplestreams.NewURLDataSource("image-metadata-url", userURL, verify)) } if custom, ok := env.(SupportsCustomSources); ok { customSources, err := custom.GetImageSources() @@ -46,7 +46,7 @@ return nil, err } if defaultURL != "" { - sources = append(sources, simplestreams.NewURLDataSource(defaultURL, simplestreams.VerifySSLHostnames)) + sources = append(sources, simplestreams.NewURLDataSource("default cloud images", defaultURL, simplestreams.VerifySSLHostnames)) } return sources, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/validation.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/validation.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,21 +11,21 @@ // ValidateImageMetadata attempts to load image metadata for the specified cloud attributes and stream // and returns any image ids found, or an error if the metadata could not be loaded. -func ValidateImageMetadata(params *simplestreams.MetadataLookupParams) ([]string, error) { +func ValidateImageMetadata(params *simplestreams.MetadataLookupParams) ([]string, *simplestreams.ResolveInfo, error) { if params.Series == "" { - return nil, fmt.Errorf("required parameter series not specified") + return nil, nil, fmt.Errorf("required parameter series not specified") } if params.Region == "" { - return nil, fmt.Errorf("required parameter region not specified") + return nil, nil, fmt.Errorf("required parameter region not specified") } if params.Endpoint == "" { - return nil, fmt.Errorf("required parameter endpoint not specified") + return nil, nil, fmt.Errorf("required parameter endpoint not specified") } if len(params.Architectures) == 0 { - return nil, fmt.Errorf("required parameter arches not specified") + return nil, nil, fmt.Errorf("required parameter arches not specified") } if len(params.Sources) == 0 { - return nil, fmt.Errorf("required parameter sources not specified") + return nil, nil, fmt.Errorf("required parameter sources not specified") } imageConstraint := NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{params.Region, params.Endpoint}, @@ -33,16 +33,16 @@ Arches: params.Architectures, Stream: params.Stream, }) - matchingImages, err := Fetch(params.Sources, simplestreams.DefaultIndexPath, imageConstraint, false) + matchingImages, resolveInfo, err := Fetch(params.Sources, simplestreams.DefaultIndexPath, imageConstraint, false) if err != nil { - return nil, err + return nil, resolveInfo, err } if len(matchingImages) == 0 { - return nil, fmt.Errorf("no matching images found for constraint %+v", imageConstraint) + return nil, resolveInfo, fmt.Errorf("no matching images found for constraint %+v", imageConstraint) } image_ids := make([]string, len(matchingImages)) for i, im := range matchingImages { image_ids[i] = im.Id } - return image_ids, nil + return image_ids, resolveInfo, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package imagemetadata_test import ( + "path" "path/filepath" gc "launchpad.net/gocheck" @@ -33,7 +34,7 @@ Region: region, Endpoint: endpoint, } - targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir, filestorage.UseDefaultTmpDir) + targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir) c.Assert(err, gc.IsNil) err = imagemetadata.MergeAndWriteMetadata(series, metadata, &cloudSpec, targetStorage) if err != nil { @@ -57,11 +58,17 @@ Endpoint: "some-auth-url", Stream: stream, Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("file://"+metadataPath, simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "file://"+metadataPath, simplestreams.VerifySSLHostnames)}, } - imageIds, err := imagemetadata.ValidateImageMetadata(params) + imageIds, resolveInfo, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.IsNil) c.Assert(imageIds, gc.DeepEquals, []string{"1234"}) + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test", + Signed: false, + IndexURL: "file://" + path.Join(metadataPath, "streams/v1/index.json"), + MirrorURL: "", + }) } func (s *ValidateSuite) TestMatch(c *gc.C) { @@ -79,9 +86,9 @@ Endpoint: "some-auth-url", Stream: stream, Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("file://"+s.metadataDir, simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "file://"+s.metadataDir, simplestreams.VerifySSLHostnames)}, } - _, err := imagemetadata.ValidateImageMetadata(params) + _, _, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.Not(gc.IsNil)) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/image.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/image.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image.go 2014-03-20 12:52:38.000000000 +0000 @@ -82,13 +82,13 @@ Id string Arch string // The type of virtualisation supported by this image. - VType string + VirtType string } // match returns true if the image can run on the supplied instance type. func (image Image) match(itype InstanceType) bool { // The virtualisation type is optional. - if itype.VType != nil && image.VType != *itype.VType { + if itype.VirtType != nil && image.VirtType != *itype.VirtType { return false } for _, arch := range itype.Arches { @@ -106,9 +106,9 @@ result := make([]Image, len(inputs)) for index, input := range inputs { result[index] = Image{ - Id: input.Id, - VType: input.VType, - Arch: input.Arch, + Id: input.Id, + VirtType: input.VirtType, + Arch: input.Arch, } } return result diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/image_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/image_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -181,7 +181,7 @@ region: "test", imageId: "ami-00000033", instanceTypes: []InstanceType{ - {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VType: &pv, Mem: 512}, + {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VirtType: &pv, Mem: 512}, }, }, { @@ -190,7 +190,7 @@ stream: "released", imageId: "ami-00000035", instanceTypes: []InstanceType{ - {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VType: &hvm, Mem: 512, CpuCores: 2}, + {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VirtType: &hvm, Mem: 512, CpuCores: 2}, }, }, { @@ -199,7 +199,7 @@ stream: "daily", imageId: "ami-10000035", instanceTypes: []InstanceType{ - {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VType: &hvm, Mem: 512, CpuCores: 2}, + {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VirtType: &hvm, Mem: 512, CpuCores: 2}, }, }, { @@ -207,7 +207,7 @@ region: "test", imageId: "ami-00000035", instanceTypes: []InstanceType{ - {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VType: &hvm, Mem: 512, CpuCores: 2}, + {Id: "1", Name: "it-1", Arches: []string{"amd64"}, VirtType: &hvm, Mem: 512, CpuCores: 2}, }, }, { @@ -240,15 +240,16 @@ Stream: t.stream, }) imageMeta, err := imagemetadata.GetLatestImageIdMetadata( - []byte(jsonImagesContent), simplestreams.NewURLDataSource("some-url", simplestreams.VerifySSLHostnames), cons) + []byte(jsonImagesContent), + simplestreams.NewURLDataSource("test", "some-url", simplestreams.VerifySSLHostnames), cons) c.Assert(err, gc.IsNil) var images []Image for _, imageMetadata := range imageMeta { im := *imageMetadata images = append(images, Image{ - Id: im.Id, - VType: im.VType, - Arch: im.Arch, + Id: im.Id, + VirtType: im.VirtType, + Arch: im.Arch, }) } spec, err := FindInstanceSpec(images, &InstanceConstraint{ @@ -286,19 +287,19 @@ itype: InstanceType{Arches: []string{"amd64", "arm"}}, match: true, }, { - image: Image{Arch: "amd64", VType: hvm}, - itype: InstanceType{Arches: []string{"amd64"}, VType: &hvm}, + image: Image{Arch: "amd64", VirtType: hvm}, + itype: InstanceType{Arches: []string{"amd64"}, VirtType: &hvm}, match: true, }, { image: Image{Arch: "arm"}, itype: InstanceType{Arches: []string{"amd64"}}, }, { - image: Image{Arch: "amd64", VType: hvm}, + image: Image{Arch: "amd64", VirtType: hvm}, itype: InstanceType{Arches: []string{"amd64"}}, match: true, }, { - image: Image{Arch: "amd64", VType: "pv"}, - itype: InstanceType{Arches: []string{"amd64"}, VType: &hvm}, + image: Image{Arch: "amd64", VirtType: "pv"}, + itype: InstanceType{Arches: []string{"amd64"}, VirtType: &hvm}, }, } @@ -318,7 +319,7 @@ { Id: "id", Storage: "storage-is-ignored", - VType: "vtype", + VirtType: "vtype", Arch: "arch", RegionAlias: "region-alias-is-ignored", RegionName: "region-name-is-ignored", @@ -327,9 +328,9 @@ } expectation := []Image{ { - Id: "id", - VType: "vtype", - Arch: "arch", + Id: "id", + VirtType: "vtype", + Arch: "arch", }, } c.Check(ImageMetadataToImages(input), gc.DeepEquals, expectation) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/instancetype.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/instancetype.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/instancetype.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/instancetype.go 2014-03-20 12:52:38.000000000 +0000 @@ -20,7 +20,7 @@ Cost uint64 RootDisk uint64 // These attributes are not supported by all clouds. - VType *string // The type of virtualisation used by the hypervisor, must match the image. + VirtType *string // The type of virtualisation used by the hypervisor, must match the image. CpuPower *uint64 Tags []string } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/instancetype_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/instancetype_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/instances/instancetype_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/instancetype_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -90,7 +90,7 @@ Mem: 23552, Cost: 1300, RootDisk: 32768, - VType: &hvm, + VirtType: &hvm, }, { Name: "cc2.8xlarge", Arches: []string{"amd64"}, @@ -99,7 +99,7 @@ Mem: 61952, Cost: 2400, RootDisk: 131072, - VType: &hvm, + VirtType: &hvm, }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/interface.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/interface.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/interface.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/interface.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,6 +15,14 @@ "launchpad.net/juju-core/state/api" ) +// EnvironCapability implements access to metadata about the capabilities +// of an environment. +type EnvironCapability interface { + // SupportedArchitectures returns the image architectures which can + // be hosted by this environment. + SupportedArchitectures() ([]string, error) +} + // A EnvironProvider represents a computing and storage provider. type EnvironProvider interface { // Prepare prepares an environment for use. Any additional @@ -63,7 +71,7 @@ Storage() storage.Storage } -// ConfigGetter implements access to an environments configuration. +// ConfigGetter implements access to an environment's configuration. type ConfigGetter interface { // Config returns the configuration data with which the Environ was created. // Note that this is not necessarily current; the canonical location @@ -116,6 +124,9 @@ // ConfigGetter allows the retrieval of the configuration data. ConfigGetter + // EnvironCapability allows access to this environment's capabilities. + EnvironCapability + // SetConfig updates the Environ's configuration. // // Calls to SetConfig do not affect the configuration of diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/jujutest/livetests.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/livetests.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/jujutest/livetests.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/livetests.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,6 +11,7 @@ "strings" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -27,13 +28,11 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju" "launchpad.net/juju-core/juju/testing" - "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" @@ -138,7 +137,7 @@ c.Assert(err, gc.IsNil) } envtesting.UploadFakeTools(c, t.Env.Storage()) - err := common.EnsureNotBootstrapped(t.Env) + err := bootstrap.EnsureNotBootstrapped(t.Env) c.Assert(err, gc.IsNil) err = bootstrap.Bootstrap(coretesting.Context(c), t.Env, cons) c.Assert(err, gc.IsNil) @@ -382,7 +381,7 @@ // already up, this has been moved into the bootstrap command. t.BootstrapOnce(c) - err := common.EnsureNotBootstrapped(t.Env) + err := bootstrap.EnsureNotBootstrapped(t.Env) c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") c.Logf("destroy env") @@ -860,7 +859,10 @@ t.PrepareOnce(c) possibleTools := envtesting.AssertUploadFakeToolsVersions(c, t.Env.Storage(), version.MustParseBinary("5.4.5-precise-amd64")) - inst, _, err := t.Env.StartInstance(constraints.Value{}, possibleTools, machineConfig) + inst, _, err := t.Env.StartInstance(environs.StartInstanceParams{ + Tools: possibleTools, + MachineConfig: machineConfig, + }) if inst != nil { err := t.Env.StopInstances([]instance.Instance{inst}) c.Check(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/jujutest/tests.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/tests.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/jujutest/tests.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/tests.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "net/http" "sort" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -21,9 +22,7 @@ "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/testing" - "launchpad.net/juju-core/provider/common" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -132,7 +131,7 @@ func (t *Tests) TestBootstrap(c *gc.C) { e := t.Prepare(c) envtesting.UploadFakeTools(c, e.Storage()) - err := common.EnsureNotBootstrapped(e) + err := bootstrap.EnsureNotBootstrapped(e) c.Assert(err, gc.IsNil) err = bootstrap.Bootstrap(coretesting.Context(c), e, constraints.Value{}) c.Assert(err, gc.IsNil) @@ -141,12 +140,12 @@ c.Check(info.Addrs, gc.Not(gc.HasLen), 0) c.Check(apiInfo.Addrs, gc.Not(gc.HasLen), 0) - err = common.EnsureNotBootstrapped(e) + err = bootstrap.EnsureNotBootstrapped(e) c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") e2 := t.Open(c) envtesting.UploadFakeTools(c, e2.Storage()) - err = common.EnsureNotBootstrapped(e2) + err = bootstrap.EnsureNotBootstrapped(e2) c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") info2, apiInfo2, err := e2.StateInfo() @@ -160,12 +159,12 @@ e3 := t.Prepare(c) envtesting.UploadFakeTools(c, e3.Storage()) - err = common.EnsureNotBootstrapped(e3) + err = bootstrap.EnsureNotBootstrapped(e3) c.Assert(err, gc.IsNil) err = bootstrap.Bootstrap(coretesting.Context(c), e3, constraints.Value{}) c.Assert(err, gc.IsNil) - err = common.EnsureNotBootstrapped(e3) + err = bootstrap.EnsureNotBootstrapped(e3) c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -1,7 +1,7 @@ // Copyright 2013 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package manual +package manual_test import ( "fmt" @@ -14,6 +14,7 @@ "launchpad.net/juju-core/environs/bootstrap" "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/filestorage" + "launchpad.net/juju-core/environs/manual" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" @@ -54,18 +55,18 @@ Environ: s.Conn.Environ, storageDir: c.MkDir(), } - storage, err := filestorage.NewFileStorageWriter(s.env.storageDir, filestorage.UseDefaultTmpDir) + storage, err := filestorage.NewFileStorageWriter(s.env.storageDir) c.Assert(err, gc.IsNil) s.env.storage = storage } -func (s *bootstrapSuite) getArgs(c *gc.C) BootstrapArgs { +func (s *bootstrapSuite) getArgs(c *gc.C) manual.BootstrapArgs { hostname, err := os.Hostname() c.Assert(err, gc.IsNil) toolsList, err := tools.FindBootstrapTools(s.Conn.Environ, tools.BootstrapToolsParams{}) c.Assert(err, gc.IsNil) arch := "amd64" - return BootstrapArgs{ + return manual.BootstrapArgs{ Host: hostname, DataDir: "/var/lib/juju", Environ: s.env, @@ -83,7 +84,7 @@ args.Host = "ubuntu@" + args.Host defer fakeSSH{SkipDetection: true}.install(c).Restore() - err := Bootstrap(args) + err := manual.Bootstrap(args) c.Assert(err, gc.IsNil) bootstrapState, err := bootstrap.LoadState(s.env.Storage()) @@ -91,14 +92,14 @@ c.Assert( bootstrapState.StateInstances, gc.DeepEquals, - []instance.Id{BootstrapInstanceId}, + []instance.Id{manual.BootstrapInstanceId}, ) // Do it all again; this should work, despite the fact that // there's a bootstrap state file. Existence for that is // checked in general bootstrap code (environs/bootstrap). defer fakeSSH{SkipDetection: true}.install(c).Restore() - err = Bootstrap(args) + err = manual.Bootstrap(args) c.Assert(err, gc.IsNil) // We *do* check that the machine has no juju* upstart jobs, though. @@ -107,15 +108,15 @@ SkipDetection: true, SkipProvisionAgent: true, }.install(c).Restore() - err = Bootstrap(args) - c.Assert(err, gc.Equals, ErrProvisioned) + err = manual.Bootstrap(args) + c.Assert(err, gc.Equals, manual.ErrProvisioned) } func (s *bootstrapSuite) TestBootstrapScriptFailure(c *gc.C) { args := s.getArgs(c) args.Host = "ubuntu@" + args.Host defer fakeSSH{SkipDetection: true, ProvisionAgentExitCode: 1}.install(c).Restore() - err := Bootstrap(args) + err := manual.Bootstrap(args) c.Assert(err, gc.NotNil) // Since the script failed, the state file should have been @@ -127,19 +128,19 @@ func (s *bootstrapSuite) TestBootstrapEmptyDataDir(c *gc.C) { args := s.getArgs(c) args.DataDir = "" - c.Assert(Bootstrap(args), gc.ErrorMatches, "data-dir argument is empty") + c.Assert(manual.Bootstrap(args), gc.ErrorMatches, "data-dir argument is empty") } func (s *bootstrapSuite) TestBootstrapEmptyHost(c *gc.C) { args := s.getArgs(c) args.Host = "" - c.Assert(Bootstrap(args), gc.ErrorMatches, "host argument is empty") + c.Assert(manual.Bootstrap(args), gc.ErrorMatches, "host argument is empty") } func (s *bootstrapSuite) TestBootstrapNilEnviron(c *gc.C) { args := s.getArgs(c) args.Environ = nil - c.Assert(Bootstrap(args), gc.ErrorMatches, "environ argument is nil") + c.Assert(manual.Bootstrap(args), gc.ErrorMatches, "environ argument is nil") } func (s *bootstrapSuite) TestBootstrapNoMatchingTools(c *gc.C) { @@ -147,13 +148,13 @@ args := s.getArgs(c) args.PossibleTools = nil defer fakeSSH{SkipDetection: true, SkipProvisionAgent: true}.install(c).Restore() - c.Assert(Bootstrap(args), gc.ErrorMatches, "possible tools is empty") + c.Assert(manual.Bootstrap(args), gc.ErrorMatches, "possible tools is empty") // Non-empty list, but none that match the series/arch. args = s.getArgs(c) args.Series = "edgy" defer fakeSSH{SkipDetection: true, SkipProvisionAgent: true}.install(c).Restore() - c.Assert(Bootstrap(args), gc.ErrorMatches, "no matching tools available") + c.Assert(manual.Bootstrap(args), gc.ErrorMatches, "no matching tools available") } func (s *bootstrapSuite) TestBootstrapToolsFileURL(c *gc.C) { @@ -170,13 +171,13 @@ } func (s *bootstrapSuite) testBootstrapToolsURL(c *gc.C, toolsURL, expectedURL string) { - s.PatchValue(&provisionMachineAgent, func(host string, mcfg *cloudinit.MachineConfig, w io.Writer) error { + s.PatchValue(manual.ProvisionMachineAgent, func(host string, mcfg *cloudinit.MachineConfig, w io.Writer) error { c.Assert(mcfg.Tools.URL, gc.Equals, expectedURL) return nil }) args := s.getArgs(c) args.PossibleTools[0].URL = toolsURL defer fakeSSH{SkipDetection: true}.install(c).Restore() - err := Bootstrap(args) + err := manual.Bootstrap(args) c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,4 +5,11 @@ var ( InstanceHostAddresses = &instanceHostAddresses + ProvisionMachineAgent = &provisionMachineAgent + CheckProvisioned = checkProvisioned +) + +const ( + DetectionScript = detectionScript + CheckProvisionedScript = checkProvisionedScript ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/fakessh.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/fakessh.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/fakessh.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/fakessh.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,87 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package manual - -import ( - "strings" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" - sshtesting "launchpad.net/juju-core/utils/ssh/testing" -) - -// installDetectionFakeSSH installs a fake SSH command, which will respond -// to the series/hardware detection script with the specified -// series/arch. -func installDetectionFakeSSH(c *gc.C, series, arch string) testbase.Restorer { - if series == "" { - series = "precise" - } - if arch == "" { - arch = "amd64" - } - detectionoutput := strings.Join([]string{ - series, - arch, - "MemTotal: 4096 kB", - "processor: 0", - }, "\n") - return sshtesting.InstallFakeSSH(c, detectionScript, detectionoutput, 0) -} - -// FakeSSH wraps the invocation of InstallFakeSSH based on the parameters. -type fakeSSH struct { - Series string - Arch string - - // Provisioned should be set to true if the fakeSSH script - // should respond to checkProvisioned with a non-empty result. - Provisioned bool - - // exit code for the checkProvisioned script. - CheckProvisionedExitCode int - - // exit code for the machine agent provisioning script. - ProvisionAgentExitCode int - - // InitUbuntuUser should be set to true if the fakeSSH script - // should respond to an attempt to initialise the ubuntu user. - InitUbuntuUser bool - - // there are conditions other than error in the above - // that might cause provisioning to not go ahead, such - // as tools being missing. - SkipProvisionAgent bool - - // detection will be skipped if the series/hardware were - // detected ahead of time. This should always be set to - // true when testing Bootstrap. - SkipDetection bool -} - -// install installs fake SSH commands, which will respond to -// manual provisioning/bootstrapping commands with the specified -// output and exit codes. -func (r fakeSSH) install(c *gc.C) testbase.Restorer { - var restore testbase.Restorer - add := func(input, output interface{}, rc int) { - restore = restore.Add(sshtesting.InstallFakeSSH(c, input, output, rc)) - } - if !r.SkipProvisionAgent { - add(nil, nil, r.ProvisionAgentExitCode) - } - if !r.SkipDetection { - restore.Add(installDetectionFakeSSH(c, r.Series, r.Arch)) - } - var checkProvisionedOutput interface{} - if r.Provisioned { - checkProvisionedOutput = "/etc/init/jujud-machine-0.conf" - } - add(checkProvisionedScript, checkProvisionedOutput, r.CheckProvisionedExitCode) - if r.InitUbuntuUser { - add("", nil, 0) - } - return restore -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/fakessh_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/fakessh_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/manual/fakessh_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/fakessh_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,156 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package manual_test + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/juju/testing" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs/manual" +) + +// sshscript should only print the result on the first execution, +// to handle the case where it's called multiple times. On +// subsequent executions, it should find the next 'ssh' in $PATH +// and exec that. +var sshscript = `#!/bin/bash --norc +if [ ! -e "$0.run" ]; then + touch "$0.run" + if [ -e "$0.expected-input" ]; then + diff "$0.expected-input" - + exitcode=$? + if [ $exitcode -ne 0 ]; then + echo "ERROR: did not match expected input" >&2 + exit $exitcode + fi + else + head >/dev/null + fi + # stdout + %s + # stderr + %s + exit %d +else + export PATH=${PATH#*:} + exec ssh $* +fi` + +// installFakeSSH creates a fake "ssh" command in a new $PATH, +// updates $PATH, and returns a function to reset $PATH to its +// original value when called. +// +// input may be: +// - nil (ignore input) +// - a string (match input exactly) +// output may be: +// - nil (no output) +// - a string (stdout) +// - a slice of strings, of length two (stdout, stderr) +func installFakeSSH(c *gc.C, input, output interface{}, rc int) testing.Restorer { + fakebin := c.MkDir() + ssh := filepath.Join(fakebin, "ssh") + switch input := input.(type) { + case nil: + case string: + sshexpectedinput := ssh + ".expected-input" + err := ioutil.WriteFile(sshexpectedinput, []byte(input), 0644) + c.Assert(err, gc.IsNil) + default: + c.Errorf("input has invalid type: %T", input) + } + var stdout, stderr string + switch output := output.(type) { + case nil: + case string: + stdout = fmt.Sprintf("cat<&2< 0 { c.Logf(actual.String()) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/urls.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/urls.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls.go 2014-03-20 12:52:38.000000000 +0000 @@ -39,7 +39,7 @@ if !config.SSLHostnameVerification() { verify = simplestreams.NoVerifySSLHostnames } - sources = append(sources, simplestreams.NewURLDataSource(userURL, verify)) + sources = append(sources, simplestreams.NewURLDataSource("tools-metadata-url", userURL, verify)) } if custom, ok := env.(SupportsCustomSources); ok { customSources, err := custom.GetToolsSources() @@ -54,7 +54,7 @@ return nil, err } if defaultURL != "" { - sources = append(sources, simplestreams.NewURLDataSource(defaultURL, simplestreams.VerifySSLHostnames)) + sources = append(sources, simplestreams.NewURLDataSource("default simplestreams", defaultURL, simplestreams.VerifySSLHostnames)) } for _, source := range sources { source.SetAllowRetry(allowRetry) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/urls_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/urls_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type URLsSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/validation.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/validation.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation.go 2014-03-20 12:52:38.000000000 +0000 @@ -20,12 +20,12 @@ // ValidateToolsMetadata attempts to load tools metadata for the specified cloud attributes and returns // any tools versions found, or an error if the metadata could not be loaded. -func ValidateToolsMetadata(params *ToolsMetadataLookupParams) ([]string, error) { +func ValidateToolsMetadata(params *ToolsMetadataLookupParams) ([]string, *simplestreams.ResolveInfo, error) { if len(params.Architectures) == 0 { - return nil, fmt.Errorf("required parameter arches not specified") + return nil, nil, fmt.Errorf("required parameter arches not specified") } if len(params.Sources) == 0 { - return nil, fmt.Errorf("required parameter sources not specified") + return nil, nil, fmt.Errorf("required parameter sources not specified") } if params.Version == "" && params.Major == 0 { params.Version = version.Current.Number.String() @@ -38,23 +38,27 @@ Arches: params.Architectures, }) } else { - toolsConstraint = NewVersionedToolsConstraint(params.Version, simplestreams.LookupParams{ + versNum, err := version.Parse(params.Version) + if err != nil { + return nil, nil, err + } + toolsConstraint = NewVersionedToolsConstraint(versNum, simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{params.Region, params.Endpoint}, Series: []string{params.Series}, Arches: params.Architectures, }) } - matchingTools, err := Fetch(params.Sources, simplestreams.DefaultIndexPath, toolsConstraint, false) + matchingTools, resolveInfo, err := Fetch(params.Sources, simplestreams.DefaultIndexPath, toolsConstraint, false) if err != nil { - return nil, err + return nil, resolveInfo, err } if len(matchingTools) == 0 { - return nil, fmt.Errorf("no matching tools found for constraint %+v", toolsConstraint) + return nil, resolveInfo, fmt.Errorf("no matching tools found for constraint %+v", toolsConstraint) } versions := make([]string, len(matchingTools)) for i, tm := range matchingTools { vers := version.Binary{version.MustParse(tm.Version), tm.Release, tm.Arch} versions[i] = vers.String() } - return versions, nil + return versions, resolveInfo, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/validation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/tools/validation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,23 +4,24 @@ package tools import ( + "path" + gc "launchpad.net/gocheck" + "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/simplestreams" - "launchpad.net/juju-core/juju/osenv" - coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" ) type ValidateSuite struct { testbase.LoggingSuite - home *coretesting.FakeHome + metadataDir string } var _ = gc.Suite(&ValidateSuite{}) -func (s *ValidateSuite) makeLocalMetadata(c *gc.C, version, region, series, endpoint string) error { - tm := ToolsMetadata{ +func (s *ValidateSuite) makeLocalMetadata(c *gc.C, version, series string) error { + tm := []*ToolsMetadata{{ Version: version, Release: series, Arch: "amd64", @@ -28,31 +29,26 @@ Size: 1234, FileType: "tar.gz", SHA256: "f65a92b3b41311bdf398663ee1c5cd0c", - } - cloudSpec := simplestreams.CloudSpec{ - Region: region, - Endpoint: endpoint, - } - _, err := MakeBoilerplate(&tm, &cloudSpec, false) - if err != nil { - return err - } + }} + + stor, err := filestorage.NewFileStorageWriter(s.metadataDir) + c.Assert(err, gc.IsNil) + err = WriteMetadata(stor, tm, false) + c.Assert(err, gc.IsNil) return nil } func (s *ValidateSuite) SetUpTest(c *gc.C) { s.LoggingSuite.SetUpTest(c) - s.home = coretesting.MakeEmptyFakeHome(c) + s.metadataDir = c.MkDir() } -func (s *ValidateSuite) TearDownTest(c *gc.C) { - s.home.Restore() - s.LoggingSuite.TearDownTest(c) +func (s *ValidateSuite) toolsURL() string { + return "file://" + path.Join(s.metadataDir, "tools") } func (s *ValidateSuite) TestExactVersionMatch(c *gc.C) { - s.makeLocalMetadata(c, "1.11.2", "region-2", "raring", "some-auth-url") - metadataDir := osenv.JujuHomePath("") + s.makeLocalMetadata(c, "1.11.2", "raring") params := &ToolsMetadataLookupParams{ Version: "1.11.2", MetadataLookupParams: simplestreams.MetadataLookupParams{ @@ -60,17 +56,23 @@ Series: "raring", Architectures: []string{"amd64"}, Endpoint: "some-auth-url", - Sources: []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+metadataDir, simplestreams.VerifySSLHostnames)}, + Sources: []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, }, } - versions, err := ValidateToolsMetadata(params) + versions, resolveInfo, err := ValidateToolsMetadata(params) c.Assert(err, gc.IsNil) c.Assert(versions, gc.DeepEquals, []string{"1.11.2-raring-amd64"}) + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test", + Signed: false, + IndexURL: "file://" + path.Join(s.metadataDir, "tools/streams/v1/index.json"), + MirrorURL: "", + }) } func (s *ValidateSuite) TestMajorVersionMatch(c *gc.C) { - s.makeLocalMetadata(c, "1.11.2", "region-2", "raring", "some-auth-url") - metadataDir := osenv.JujuHomePath("") + s.makeLocalMetadata(c, "1.11.2", "raring") params := &ToolsMetadataLookupParams{ Major: 1, Minor: -1, @@ -79,17 +81,23 @@ Series: "raring", Architectures: []string{"amd64"}, Endpoint: "some-auth-url", - Sources: []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+metadataDir, simplestreams.VerifySSLHostnames)}, + Sources: []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, }, } - versions, err := ValidateToolsMetadata(params) + versions, resolveInfo, err := ValidateToolsMetadata(params) c.Assert(err, gc.IsNil) c.Assert(versions, gc.DeepEquals, []string{"1.11.2-raring-amd64"}) + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test", + Signed: false, + IndexURL: "file://" + path.Join(s.metadataDir, "tools/streams/v1/index.json"), + MirrorURL: "", + }) } func (s *ValidateSuite) TestMajorMinorVersionMatch(c *gc.C) { - s.makeLocalMetadata(c, "1.11.2", "region-2", "raring", "some-auth-url") - metadataDir := osenv.JujuHomePath("") + s.makeLocalMetadata(c, "1.11.2", "raring") params := &ToolsMetadataLookupParams{ Major: 1, Minor: 11, @@ -98,17 +106,23 @@ Series: "raring", Architectures: []string{"amd64"}, Endpoint: "some-auth-url", - Sources: []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+metadataDir, simplestreams.VerifySSLHostnames)}, + Sources: []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, }, } - versions, err := ValidateToolsMetadata(params) + versions, resolveInfo, err := ValidateToolsMetadata(params) c.Assert(err, gc.IsNil) c.Assert(versions, gc.DeepEquals, []string{"1.11.2-raring-amd64"}) + c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ + Source: "test", + Signed: false, + IndexURL: "file://" + path.Join(s.metadataDir, "tools/streams/v1/index.json"), + MirrorURL: "", + }) } func (s *ValidateSuite) TestNoMatch(c *gc.C) { - s.makeLocalMetadata(c, "1.11.2", "region-2", "raring", "some-auth-url") - metadataDir := osenv.JujuHomePath("") + s.makeLocalMetadata(c, "1.11.2", "raring") params := &ToolsMetadataLookupParams{ Version: "1.11.2", MetadataLookupParams: simplestreams.MetadataLookupParams{ @@ -116,9 +130,10 @@ Series: "precise", Architectures: []string{"amd64"}, Endpoint: "some-auth-url", - Sources: []simplestreams.DataSource{simplestreams.NewURLDataSource("file://"+metadataDir, simplestreams.VerifySSLHostnames)}, + Sources: []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, }, } - _, err := ValidateToolsMetadata(params) + _, _, err := ValidateToolsMetadata(params) c.Assert(err, gc.Not(gc.IsNil)) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/environs/utils.go juju-core-1.17.6/src/launchpad.net/juju-core/environs/utils.go --- juju-core-1.17.4/src/launchpad.net/juju-core/environs/utils.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/environs/utils.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,22 @@ +package environs + +import ( + "fmt" + + "launchpad.net/juju-core/environs/storage" + "launchpad.net/juju-core/state" +) + +// GetStorage creates an Environ from the config in state and returns +// its storage interface. +func GetStorage(st *state.State) (storage.Storage, error) { + envConfig, err := st.EnvironConfig() + if err != nil { + return nil, fmt.Errorf("cannot get environment config: %v", err) + } + env, err := New(envConfig) + if err != nil { + return nil, fmt.Errorf("cannot access environment: %v", err) + } + return env.Storage(), nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/errors/errors_test.go juju-core-1.17.6/src/launchpad.net/juju-core/errors/errors_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/errors/errors_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/errors/errors_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,10 +9,10 @@ "runtime" "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" - jc "launchpad.net/juju-core/testing/checkers" ) type errorsSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/etc/bash_completion.d/juju-core juju-core-1.17.6/src/launchpad.net/juju-core/etc/bash_completion.d/juju-core --- juju-core-1.17.4/src/launchpad.net/juju-core/etc/bash_completion.d/juju-core 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/etc/bash_completion.d/juju-core 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,146 @@ +#!/bin/bash +# juju-core.bash_completion.sh: dynamic bash completion for juju cmdline, +# from parsed (and cached) juju status output. +# +# Author: JuanJo Ciarlante +# Copyright 2013+, Canonical Ltd. +# License: GPLv3 +# + +# Print (return) all machines +_juju_machines_from_file() { +python -c ' +import json, sys; j=json.load(sys.stdin) +print "\n".join(j["machines"].keys());' < ${1?} +} + +# Print (return) all units, each optionally postfixed by $2 (eg. 'myservice/0:') +_juju_units_from_file() { +python -c ' +trail="'${2}'" +import json, sys; j=json.load(sys.stdin) +all_units=[] +for k,v in j["services"].items(): + if v.get("units"): + all_units.extend(v.get("units",{}).keys()) +print "\n".join([unit + trail for unit in all_units]) +' < ${1?} +} + +# Print (return) all services +_juju_services_from_file() { +python -c ' +import json, sys; j=json.load(sys.stdin) +print "\n".join(j["services"].keys());' < ${1?} +} + +# Print (return) both services and units, currently used for juju status completion +_juju_services_and_units_from_file() { + _juju_services_from_file "$@" + _juju_units_from_file "$@" +} + +# Print (return) all juju commands +_juju_list_commands() { + juju help commands 2>/dev/null | awk '{print $1}' +} + +# Print (return) flags for juju action, shamelessly excluding +# -e/--environment for cleaner completion for common usage cases +# (e.g. juju ssh , etc) +_juju_flags_for() { + test -z "${1}" && return 0 + juju help ${1} 2>/dev/null |egrep -o -- '(^|-)-[a-z-]+'|egrep -v -- '^(-e|--environment)'|sort -u +} + +# Print (return) guessed completion function for cmd. +# Guessing is done by parsing 1st line of juju help , +# see case switch below. +_juju_completion_func_for_cmd() { + local action=${1} cword=${2} + # if cword==1 or action==help, use _juju_list_commands + if [ "${cword}" -eq 1 -o "${action}" = help ]; then + echo _juju_list_commands + return 0 + fi + # parse 1st line of juju help , to guess the completion function + case $(juju help ${action} 2>/dev/null| head -1) in + # special case for ssh, scp which have 'service' in 1st line of help: + *\ /dev/null) ]]; then + # ... create it + juju status --format=json > "${juju_status_file}".tmp && \ + mv "${juju_status_file}".tmp "${juju_status_file}" + rm -f "${juju_status_file}".tmp + fi + if [ -r "${juju_status_file}" ]; then + echo "${juju_status_file}" + else + return 1 + fi +} +# Main completion function wrap: +# calls passed completion function, also adding flags for cmd +_juju_complete_with_func() { + local action="${1}" func=${2?} + local cur + + # scp is special, as we want ':' appended to unit names, + # and filename completion also. + local postfix_str= compgen_xtra= + if [ "${action}" = "scp" ]; then + local orig_comp_wordbreaks="${COMP_WORDBREAKS}" + COMP_WORDBREAKS="${COMP_WORDBREAKS/:/}" + postfix_str=':' + compgen_xtra='-A file' + compopt -o nospace + fi + juju_status_file= + # if func name ends with 'from_file', set juju_status_file + [[ ${func} =~ .*from_file ]] && juju_status_file=$(_juju_get_status_filename) + # build COMPREPLY from passed function stdout, and _juju_flags_for $action + cur="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=( $( compgen ${compgen_xtra} -W "$(${func} ${juju_status_file} ${postfix_str}) $(_juju_flags_for "${action}")" -- ${cur} )) + if [ "${action}" = "scp" ]; then + COMP_WORDBREAKS="${orig_comp_wordbreaks}" + compopt +o nospace + fi + return 0 +} + +# Not used here, available to the user for quick cache removal +_juju_completion_cache_rm() { + rm -fv $HOME/.cache/juju/juju-status-${JUJU_ENV:-default} +} + +# main completion function entry point +_juju() { + local action parsing_func + action="${COMP_WORDS[1]}" + COMPREPLY=() + parsing_func=$(_juju_completion_func_for_cmd "${action}" ${COMP_CWORD}) + test -z "${parsing_func}" && return 0 + _juju_complete_with_func "${action}" "${parsing_func}" + return $? +} +complete -F _juju juju +# vim: ai et sw=2 ts=2 diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/instance/address.go juju-core-1.17.6/src/launchpad.net/juju-core/instance/address.go --- juju-core-1.17.4/src/launchpad.net/juju-core/instance/address.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/instance/address.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "bytes" "net" + "strconv" ) // AddressType represents the possible ways of specifying a machine location by @@ -40,6 +41,31 @@ NetworkScope } +// HostPort associates an address with a port. +type HostPort struct { + Address + Port int +} + +// AddressesWithPort returns the given addresses all +// associated with the given port. +func AddressesWithPort(addrs []Address, port int) []HostPort { + hps := make([]HostPort, len(addrs)) + for i, addr := range addrs { + hps[i] = HostPort{ + Address: addr, + Port: port, + } + } + return hps +} + +// NetAddr returns the host-port as an address +// suitable for calling net.Dial. +func (hp HostPort) NetAddr() string { + return net.JoinHostPort(hp.Value, strconv.Itoa(hp.Port)) +} + // String returns a string representation of the address, // in the form: scope:address(network name); // for example: @@ -64,6 +90,14 @@ return buf.String() } +// NewAddresses is a convenience function to create addresses from a string slice +func NewAddresses(inAddresses []string) (outAddresses []Address) { + for _, address := range inAddresses { + outAddresses = append(outAddresses, NewAddress(address)) + } + return outAddresses +} + func DeriveAddressType(value string) AddressType { ip := net.ParseIP(value) if ip != nil { @@ -126,40 +160,90 @@ // be appropriate to display as a publicly accessible endpoint. // If there are no suitable addresses, the empty string is returned. func SelectPublicAddress(addresses []Address) string { - mostpublic := "" - for _, addr := range addresses { + index := publicAddressIndex(len(addresses), func(i int) Address { + return addresses[i] + }) + if index < 0 { + return "" + } + return addresses[index].Value +} + +func SelectPublicHostPort(hps []HostPort) string { + index := publicAddressIndex(len(hps), func(i int) Address { + return hps[i].Address + }) + if index < 0 { + return "" + } + return hps[index].NetAddr() +} + +// publicAddressIndex is the internal version of SelectPublicAddress. +// It returns the index the selected address, or -1 if not found. +func publicAddressIndex(numAddr int, getAddr func(i int) Address) int { + mostPublicIndex := -1 + for i := 0; i < numAddr; i++ { + addr := getAddr(i) if addr.Type != Ipv6Address { switch addr.NetworkScope { case NetworkPublic: - return addr.Value + return i case NetworkCloudLocal, NetworkUnknown: - mostpublic = addr.Value + mostPublicIndex = i } } } - return mostpublic + return mostPublicIndex } // SelectInternalAddress picks one address from a slice that can be // used as an endpoint for juju internal communication. // If there are no suitable addresses, the empty string is returned. func SelectInternalAddress(addresses []Address, machineLocal bool) string { - usableAddress := "" - for _, addr := range addresses { + index := internalAddressIndex(len(addresses), func(i int) Address { + return addresses[i] + }, machineLocal) + if index < 0 { + return "" + } + return addresses[index].Value +} + +// SelectInternalAddress picks one HostPort from a slice that can be +// used as an endpoint for juju internal communication +// and returns it in its NetAddr form. +// If there are no suitable addresses, the empty string is returned. +func SelectInternalHostPort(hps []HostPort, machineLocal bool) string { + index := internalAddressIndex(len(hps), func(i int) Address { + return hps[i].Address + }, machineLocal) + if index < 0 { + return "" + } + return hps[index].NetAddr() +} + +// internalAddressIndex is the internal version of SelectInternalAddress. +// It returns the index the selected address, or -1 if not found. +func internalAddressIndex(numAddr int, getAddr func(i int) Address, machineLocal bool) int { + usableAddressIndex := -1 + for i := 0; i < numAddr; i++ { + addr := getAddr(i) if addr.Type != Ipv6Address { switch addr.NetworkScope { case NetworkCloudLocal: - return addr.Value + return i case NetworkMachineLocal: if machineLocal { - return addr.Value + return i } case NetworkPublic, NetworkUnknown: - if usableAddress == "" { - usableAddress = addr.Value + if usableAddressIndex == -1 { + usableAddressIndex = i } } } } - return usableAddress + return usableAddressIndex } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/instance/address_test.go juju-core-1.17.6/src/launchpad.net/juju-core/instance/address_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/instance/address_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/instance/address_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "errors" "net" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing/testbase" @@ -30,40 +31,86 @@ c.Check(addr.Type, gc.Equals, Ipv6Address) } +func (s *AddressSuite) TestNewAddresses(c *gc.C) { + addresses := NewAddresses( + []string{"127.0.0.1", "192.168.1.1", "192.168.178.255"}) + c.Assert(len(addresses), gc.Equals, 3) + c.Assert(addresses[0].Value, gc.Equals, "127.0.0.1") + c.Assert(addresses[1].Value, gc.Equals, "192.168.1.1") + c.Assert(addresses[2].Value, gc.Equals, "192.168.178.255") +} + func (s *AddressSuite) TestNewAddressHostname(c *gc.C) { addr := NewAddress("localhost") c.Check(addr.Value, gc.Equals, "localhost") c.Check(addr.Type, gc.Equals, HostName) } -type selectTests struct { - about string - addresses []Address - expected string +type selectTest struct { + about string + addresses []Address + expectedIndex int +} + +// expected returns the expected address for the test. +func (t selectTest) expected() string { + if t.expectedIndex == -1 { + return "" + } + return t.addresses[t.expectedIndex].Value } -var selectPublicTests = []selectTests{{ +type hostPortTest struct { + about string + hostPorts []HostPort + expectedIndex int +} + +// hostPortTest returns the HostPort equivalent test to the +// receiving selectTest. +func (t selectTest) hostPortTest() hostPortTest { + hps := AddressesWithPort(t.addresses, 9999) + for i := range hps { + hps[i].Port = i + 1 + } + return hostPortTest{ + about: t.about, + hostPorts: hps, + expectedIndex: t.expectedIndex, + } +} + +// expected returns the expected host:port result +// of the test. +func (t hostPortTest) expected() string { + if t.expectedIndex == -1 { + return "" + } + return t.hostPorts[t.expectedIndex].NetAddr() +} + +var selectPublicTests = []selectTest{{ "no addresses gives empty string result", []Address{}, - "", + -1, }, { "a public address is selected", []Address{ {"8.8.8.8", Ipv4Address, "public", NetworkPublic}, }, - "8.8.8.8", + 0, }, { "a machine local address is not selected", []Address{ {"127.0.0.1", Ipv4Address, "machine", NetworkMachineLocal}, }, - "", + -1, }, { "an ipv6 address is not selected", []Address{ {"2001:DB8::1", Ipv6Address, "", NetworkPublic}, }, - "", + -1, }, { "a public name is preferred to an unknown or cloud local address", []Address{ @@ -71,85 +118,109 @@ {"10.0.0.1", Ipv4Address, "cloud", NetworkCloudLocal}, {"public.invalid.testing", HostName, "public", NetworkPublic}, }, - "public.invalid.testing", + 2, }, { "last unknown address selected", []Address{ {"10.0.0.1", Ipv4Address, "cloud", NetworkUnknown}, {"8.8.8.8", Ipv4Address, "floating", NetworkUnknown}, }, - "8.8.8.8", + 1, }} -func (s *AddressSuite) TestSelectPublicAddressEmpty(c *gc.C) { +func (s *AddressSuite) TestSelectPublicAddress(c *gc.C) { for i, t := range selectPublicTests { c.Logf("test %d. %s", i, t.about) - c.Check(SelectPublicAddress(t.addresses), gc.Equals, t.expected) + c.Check(SelectPublicAddress(t.addresses), gc.Equals, t.expected()) + } +} + +func (s *AddressSuite) TestSelectPublicHostPort(c *gc.C) { + for i, t0 := range selectPublicTests { + t := t0.hostPortTest() + c.Logf("test %d. %s", i, t.about) + c.Assert(SelectPublicHostPort(t.hostPorts), gc.DeepEquals, t.expected()) } } -var selectInternalTests = []selectTests{{ +var selectInternalTests = []selectTest{{ "no addresses gives empty string result", []Address{}, - "", + -1, }, { "a public address is selected", []Address{ {"8.8.8.8", Ipv4Address, "public", NetworkPublic}, }, - "8.8.8.8", + 0, }, { "a cloud local address is selected", []Address{ {"10.0.0.1", Ipv4Address, "private", NetworkCloudLocal}, }, - "10.0.0.1", + 0, }, { "a machine local address is not selected", []Address{ {"127.0.0.1", Ipv4Address, "machine", NetworkMachineLocal}, }, - "", + -1, }, { "ipv6 addresses are not selected", []Address{ {"::1", Ipv6Address, "", NetworkCloudLocal}, }, - "", + -1, }, { "a cloud local address is preferred to a public address", []Address{ {"10.0.0.1", Ipv4Address, "cloud", NetworkCloudLocal}, {"8.8.8.8", Ipv4Address, "public", NetworkPublic}, }, - "10.0.0.1", + 0, }} func (s *AddressSuite) TestSelectInternalAddress(c *gc.C) { for i, t := range selectInternalTests { c.Logf("test %d. %s", i, t.about) - c.Check(SelectInternalAddress(t.addresses, false), gc.Equals, t.expected) + c.Check(SelectInternalAddress(t.addresses, false), gc.Equals, t.expected()) + } +} + +func (s *AddressSuite) TestSelectInternalHostPort(c *gc.C) { + for i, t0 := range selectInternalTests { + t := t0.hostPortTest() + c.Logf("test %d. %s", i, t.about) + c.Assert(SelectInternalHostPort(t.hostPorts, false), gc.DeepEquals, t.expected()) } } -var selectInternalMachineTests = []selectTests{{ +var selectInternalMachineTests = []selectTest{{ "a cloud local address is selected", []Address{ {"10.0.0.1", Ipv4Address, "cloud", NetworkCloudLocal}, }, - "10.0.0.1", + 0, }, { "a machine local address is selected", []Address{ {"127.0.0.1", Ipv4Address, "container", NetworkMachineLocal}, }, - "127.0.0.1", + 0, }} func (s *AddressSuite) TestSelectInternalMachineAddress(c *gc.C) { for i, t := range selectInternalMachineTests { c.Logf("test %d. %s", i, t.about) - c.Check(SelectInternalAddress(t.addresses, true), gc.Equals, t.expected) + c.Check(SelectInternalAddress(t.addresses, true), gc.Equals, t.expected()) + } +} + +func (s *AddressSuite) TestSelectInternalMachineHostPort(c *gc.C) { + for i, t0 := range selectInternalMachineTests { + t := t0.hostPortTest() + c.Logf("test %d. %s", i, t.about) + c.Assert(SelectInternalHostPort(t.hostPorts, true), gc.DeepEquals, t.expected()) } } @@ -234,3 +305,40 @@ c.Check(test.addr.String(), gc.Equals, test.str) } } + +func (*AddressSuite) TestAddressesWithPort(c *gc.C) { + addrs := NewAddresses([]string{"0.1.2.3", "0.2.4.6"}) + hps := AddressesWithPort(addrs, 999) + c.Assert(hps, jc.DeepEquals, []HostPort{{ + Address: NewAddress("0.1.2.3"), + Port: 999, + }, { + Address: NewAddress("0.2.4.6"), + Port: 999, + }}) +} + +var netAddrTests = []struct { + addr Address + port int + expect string +}{{ + addr: NewAddress("0.1.2.3"), + port: 99, + expect: "0.1.2.3:99", +}, { + addr: NewAddress("2001:DB8::1"), + port: 100, + expect: "[2001:DB8::1]:100", +}} + +func (*AddressSuite) TestNetAddr(c *gc.C) { + for i, test := range netAddrTests { + c.Logf("test %d: %q", i, test.addr) + hp := HostPort{ + Address: test.addr, + Port: test.port, + } + c.Assert(hp.NetAddr(), gc.Equals, test.expect) + } +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/instance/instance.go juju-core-1.17.6/src/launchpad.net/juju-core/instance/instance.go --- juju-core-1.17.4/src/launchpad.net/juju-core/instance/instance.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/instance/instance.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,8 @@ "sort" "strconv" "strings" + + "launchpad.net/juju-core/juju/arch" ) var ErrNoDNSName = errors.New("DNS name not allocated") @@ -173,10 +175,7 @@ if hc.Arch != nil { return fmt.Errorf("already set") } - switch str { - case "": - case "amd64", "i386", "arm": - default: + if str != "" && !arch.IsSupportedArch(str) { return fmt.Errorf("%q not recognized", str) } hc.Arch = &str diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/apiconn_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/apiconn_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/apiconn_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/apiconn_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,8 @@ "os" "time" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -21,7 +23,6 @@ "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/state/api" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -85,14 +86,14 @@ cs.LoggingSuite.TearDownTest(c) } -func (*NewAPIClientSuite) TestNameDefault(c *gc.C) { +func (s *NewAPIClientSuite) TestNameDefault(c *gc.C) { defer coretesting.MakeMultipleEnvHome(c).Restore() // The connection logic should not delay the config connection // at all when there is no environment info available. // Make sure of that by providing a suitably long delay // and checking that the connection happens within that // time. - defer testbase.PatchValue(juju.ProviderConnectDelay, coretesting.LongWait).Restore() + s.PatchValue(juju.ProviderConnectDelay, coretesting.LongWait) bootstrapEnv(c, coretesting.SampleEnvName, defaultConfigStore(c)) startTime := time.Now() @@ -115,7 +116,7 @@ assertEnvironmentName(c, apiclient, envName) } -func (*NewAPIClientSuite) TestWithInfoOnly(c *gc.C) { +func (s *NewAPIClientSuite) TestWithInfoOnly(c *gc.C) { defer coretesting.MakeEmptyFakeHome(c).Restore() storeConfig := &environInfo{ creds: configstore.APICredentials{ @@ -139,18 +140,18 @@ called++ return expectState, nil } - // Give NewAPIFromName a store interface that can report when the + // Give NewAPIFromStore a store interface that can report when the // config was written to, to ensure the cache isn't updated. - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() + s.PatchValue(juju.APIOpen, apiOpen) mockStore := &storageWithWriteNotify{store: store} - st, err := juju.NewAPIFromName("noconfig", mockStore) + st, err := juju.NewAPIFromStore("noconfig", mockStore) c.Assert(err, gc.IsNil) c.Assert(st, gc.Equals, expectState) c.Assert(called, gc.Equals, 1) c.Assert(mockStore.written, jc.IsFalse) } -func (*NewAPIClientSuite) TestWithConfigAndNoInfo(c *gc.C) { +func (s *NewAPIClientSuite) TestWithConfigAndNoInfo(c *gc.C) { defer coretesting.MakeSampleHome(c).Restore() store := newConfigStore(coretesting.SampleEnvName, &environInfo{ @@ -185,8 +186,8 @@ called++ return expectState, nil } - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() - st, err := juju.NewAPIFromName("myenv", store) + s.PatchValue(juju.APIOpen, apiOpen) + st, err := juju.NewAPIFromStore("myenv", store) c.Assert(err, gc.IsNil) c.Assert(st, gc.Equals, expectState) c.Assert(called, gc.Equals, 1) @@ -204,12 +205,12 @@ c.Check(creds.Password, gc.Equals, "adminpass") } -func (*NewAPIClientSuite) TestWithInfoError(c *gc.C) { +func (s *NewAPIClientSuite) TestWithInfoError(c *gc.C) { defer coretesting.MakeEmptyFakeHome(c).Restore() expectErr := fmt.Errorf("an error") store := newConfigStoreWithError(expectErr) - defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() - client, err := juju.NewAPIFromName("noconfig", store) + s.PatchValue(juju.APIOpen, panicAPIOpen) + client, err := juju.NewAPIFromStore("noconfig", store) c.Assert(err, gc.Equals, expectErr) c.Assert(client, gc.IsNil) } @@ -218,7 +219,7 @@ panic("api.Open called unexpectedly") } -func (*NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) { +func (s *NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) { defer coretesting.MakeEmptyFakeHome(c).Restore() store := newConfigStore("noconfig", &environInfo{ endpoint: configstore.APIEndpoint{ @@ -226,14 +227,14 @@ CACert: "certificated", }, }) - defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() + s.PatchValue(juju.APIOpen, panicAPIOpen) - st, err := juju.NewAPIFromName("noconfig", store) + st, err := juju.NewAPIFromStore("noconfig", store) c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`) c.Assert(st, gc.IsNil) } -func (*NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) { +func (s *NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) { defer coretesting.MakeEmptyFakeHome(c).Restore() store := newConfigStore("noconfig", &environInfo{ endpoint: configstore.APIEndpoint{ @@ -245,13 +246,13 @@ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { return nil, expectErr } - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() - st, err := juju.NewAPIFromName("noconfig", store) + s.PatchValue(juju.APIOpen, apiOpen) + st, err := juju.NewAPIFromStore("noconfig", store) c.Assert(err, gc.Equals, expectErr) c.Assert(st, gc.IsNil) } -func (*NewAPIClientSuite) TestWithSlowInfoConnect(c *gc.C) { +func (s *NewAPIClientSuite) TestWithSlowInfoConnect(c *gc.C) { defer coretesting.MakeSampleHome(c).Restore() store := configstore.NewMem() bootstrapEnv(c, coretesting.SampleEnvName, store) @@ -263,7 +264,7 @@ // On a sample run with no delay, the logic took 45ms to run, so // we make the delay slightly more than that, so that if the // logic doesn't delay at all, the test will fail reasonably consistently. - defer testbase.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond).Restore() + s.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond) apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { if info.Addrs[0] == "infoapi.invalid" { infoEndpointOpened <- struct{}{} @@ -271,13 +272,13 @@ } return cfgOpenedState, nil } - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() + s.PatchValue(juju.APIOpen, apiOpen) stateClosed, restoreAPIClose := setAPIClosed() defer restoreAPIClose.Restore() startTime := time.Now() - st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) c.Assert(err, gc.IsNil) // The connection logic should wait for some time before opening // the API from the configuration. @@ -312,7 +313,7 @@ c.Assert(err, gc.IsNil) } -func (*NewAPIClientSuite) TestWithSlowConfigConnect(c *gc.C) { +func (s *NewAPIClientSuite) TestWithSlowConfigConnect(c *gc.C) { defer coretesting.MakeSampleHome(c).Restore() store := configstore.NewMem() @@ -324,7 +325,7 @@ cfgOpenedState := new(api.State) cfgEndpointOpened := make(chan struct{}) - defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore() + s.PatchValue(juju.ProviderConnectDelay, 0*time.Second) apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { if info.Addrs[0] == "infoapi.invalid" { infoEndpointOpened <- struct{}{} @@ -335,14 +336,14 @@ <-cfgEndpointOpened return cfgOpenedState, nil } - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() + s.PatchValue(juju.APIOpen, apiOpen) stateClosed, restoreAPIClose := setAPIClosed() defer restoreAPIClose.Restore() done := make(chan struct{}) go func() { - st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) c.Check(err, gc.IsNil) c.Check(st, gc.Equals, infoOpenedState) close(done) @@ -360,7 +361,7 @@ c.Fatalf("api never opened via config") } // Let the info endpoint open go ahead and - // check that the NewAPIFromName call returns. + // check that the NewAPIFromStore call returns. infoEndpointOpened <- struct{}{} select { case <-done: @@ -379,21 +380,21 @@ } } -func (*NewAPIClientSuite) TestBothError(c *gc.C) { +func (s *NewAPIClientSuite) TestBothError(c *gc.C) { defer coretesting.MakeSampleHome(c).Restore() store := configstore.NewMem() bootstrapEnv(c, coretesting.SampleEnvName, store) setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") - defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore() + s.PatchValue(juju.ProviderConnectDelay, 0*time.Second) apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { if info.Addrs[0] == "infoapi.invalid" { return nil, fmt.Errorf("info connect failed") } return nil, fmt.Errorf("config connect failed") } - defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() - st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) + s.PatchValue(juju.APIOpen, apiOpen) + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) c.Check(err, gc.ErrorMatches, "config connect failed") c.Check(st, gc.IsNil) } @@ -426,7 +427,7 @@ err = os.Remove(osenv.JujuHomePath("environments.yaml")) c.Assert(err, gc.IsNil) - st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) c.Check(err, gc.IsNil) st.Close() } @@ -454,7 +455,7 @@ // Now we have info for envName2 which will actually // cause a connection to the originally bootstrapped // state. - st, err := juju.NewAPIFromName(envName2, store) + st, err := juju.NewAPIFromStore(envName2, store) c.Check(err, gc.IsNil) st.Close() @@ -464,7 +465,7 @@ // Disable for now until an upcoming branch fixes it. // err = info2.Destroy() // c.Assert(err, gc.IsNil) - // st, err = juju.NewAPIFromName(envName2, store) + // st, err = juju.NewAPIFromStore(envName2, store) // if err == nil { // st.Close() // } @@ -477,13 +478,13 @@ c.Assert(envInfo.Name, gc.Equals, expectName) } -func setAPIClosed() (<-chan *api.State, testbase.Restorer) { +func setAPIClosed() (<-chan *api.State, testing.Restorer) { stateClosed := make(chan *api.State) apiClose := func(st *api.State) error { stateClosed <- st return nil } - return stateClosed, testbase.PatchValue(juju.APIClose, apiClose) + return stateClosed, testing.PatchValue(juju.APIClose, apiClose) } func updateSecretsNoop(_ environs.Environ, _ *api.State) error { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/api.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/api.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/api.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/api.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "io" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" @@ -100,17 +100,24 @@ return keymanager.NewClient(st), nil } +// NewAPIFromName returns an api.State connected to the API Server for +// the named environment. If envName is "", the default environment will +// be used. +func NewAPIFromName(envName string) (*api.State, error) { + return newAPIClient(envName) +} + func newAPIClient(envName string) (*api.State, error) { store, err := configstore.NewDisk(osenv.JujuHome()) if err != nil { return nil, err } - return newAPIFromName(envName, store) + return newAPIFromStore(envName, store) } -// newAPIFromName implements the bulk of NewAPIClientFromName +// newAPIFromStore implements the bulk of NewAPIClientFromName // but is separate for testing purposes. -func newAPIFromName(envName string, store configstore.Storage) (*api.State, error) { +func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) { // Try to read the default environment configuration file. // If it doesn't exist, we carry on in case // there's some environment info for that environment. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/arch.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/arch.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/arch.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/arch.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,73 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package arch + +import ( + "regexp" + "runtime" + "strings" +) + +// The following constants define the machine architectures supported by Juju. +const ( + AMD64 = "amd64" + I386 = "i386" + ARM = "arm" + ARM64 = "arm64" + PPC64 = "ppc64" +) + +// AllSupportedArches records the machine architectures recognised by Juju. +var AllSupportedArches = []string{ + AMD64, + I386, + ARM, + ARM64, + PPC64, +} + +// archREs maps regular expressions for matching +// `uname -m` to architectures recognised by Juju. +var archREs = []struct { + *regexp.Regexp + arch string +}{ + {regexp.MustCompile("amd64|x86_64"), AMD64}, + {regexp.MustCompile("i?[3-9]86"), I386}, + {regexp.MustCompile("armv.*"), ARM}, + {regexp.MustCompile("aarch64"), ARM64}, + {regexp.MustCompile("ppc64el|ppc64le"), PPC64}, +} + +// Override for testing. +var HostArch = hostArch + +// HostArch returns the Juju architecture of the machine on which it is run. +func hostArch() string { + return NormaliseArch(runtime.GOARCH) +} + +// NormaliseArch returns the Juju architecture corresponding to a machine's +// reported architecture. The Juju architecture is used to filter simple +// streams lookup of tools and images. +func NormaliseArch(rawArch string) string { + rawArch = strings.TrimSpace(rawArch) + for _, re := range archREs { + if re.Match([]byte(rawArch)) { + return re.arch + break + } + } + return rawArch +} + +// IsSupportedArch returns true if arch is one supported by Juju. +func IsSupportedArch(arch string) bool { + for _, a := range AllSupportedArches { + if a == arch { + return true + } + } + return false +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/arch_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/arch_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/arch_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/arch_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,52 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package arch_test + +import ( + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/juju/arch" + "launchpad.net/juju-core/testing/testbase" +) + +type archSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&archSuite{}) + +func (s *archSuite) TestHostArch(c *gc.C) { + a := arch.HostArch() + c.Assert(arch.IsSupportedArch(a), jc.IsTrue) +} + +func (s *archSuite) TestNormaliseArch(c *gc.C) { + for _, test := range []struct { + raw string + arch string + }{ + {"windows", "windows"}, + {"amd64", "amd64"}, + {"x86_64", "amd64"}, + {"386", "i386"}, + {"i386", "i386"}, + {"i486", "i386"}, + {"armv", "arm"}, + {"armv7", "arm"}, + {"aarch64", "arm64"}, + {"ppc64el", "ppc64"}, + {"ppc64le", "ppc64"}, + } { + arch := arch.NormaliseArch(test.raw) + c.Check(arch, gc.Equals, test.arch) + } +} + +func (s *archSuite) TestIsSupportedArch(c *gc.C) { + for _, a := range arch.AllSupportedArches { + c.Assert(arch.IsSupportedArch(a), jc.IsTrue) + } + c.Assert(arch.IsSupportedArch("invalid"), jc.IsFalse) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/arch/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/arch/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the LGPLv3, see COPYING and COPYING.LESSER file for details. + +package arch_test + +import ( + "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/conn.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/conn.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,6 @@ "launchpad.net/juju-core/charm" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/juju/osenv" @@ -144,20 +143,17 @@ if err != nil { return err } + secretAttrs := make(map[string]interface{}) attrs := cfg.AllAttrs() for k, v := range secrets { if _, exists := attrs[k]; exists { // Environment already has secrets. Won't send again. return nil } else { - attrs[k] = v + secretAttrs[k] = v } } - newcfg, err := config.New(config.NoDefaults, attrs) - if err != nil { - return err - } - return c.State.SetEnvironConfig(newcfg, cfg) + return c.State.UpdateEnvironConfig(secretAttrs, nil, nil) } // PutCharm uploads the given charm to provider storage, and adds a diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/conn_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/conn_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,6 +12,7 @@ "strings" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -29,7 +30,6 @@ "launchpad.net/juju-core/provider/dummy" "launchpad.net/juju-core/state" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/set" @@ -124,7 +124,7 @@ c.Assert(conn.Environ.Name(), gc.Equals, envName) } -func (cs *NewConnSuite) TestConnStateSecretsSideEffect(c *gc.C) { +func (*NewConnSuite) TestConnStateSecretsSideEffect(c *gc.C) { attrs := dummy.SampleConfig().Merge(coretesting.Attrs{ "admin-secret": "side-effect secret", "secret": "pork", @@ -140,7 +140,8 @@ info, _, err := env.StateInfo() c.Assert(err, gc.IsNil) info.Password = utils.UserPasswordHash("side-effect secret", utils.CompatSalt) - st, err := state.Open(info, state.DefaultDialOpts(), environs.NewStatePolicy()) + // Use a state without a nil policy, which will allow us to set an invalid config. + st, err := state.Open(info, state.DefaultDialOpts(), state.Policy(nil)) c.Assert(err, gc.IsNil) defer assertClose(c, st) @@ -151,15 +152,8 @@ // Remove the secret from state, and then make sure it gets // pushed back again. - attrs = statecfg.AllAttrs() - delete(attrs, "secret") - newcfg, err := config.New(config.NoDefaults, attrs) + err = st.UpdateEnvironConfig(map[string]interface{}{}, []string{"secret"}, nil) c.Assert(err, gc.IsNil) - err = st.SetEnvironConfig(newcfg, statecfg) - c.Assert(err, gc.IsNil) - statecfg, err = st.EnvironConfig() - c.Assert(err, gc.IsNil) - c.Assert(statecfg.UnknownAttrs()["secret"], gc.IsNil) // Make a new Conn, which will push the secrets. conn, err := juju.NewConn(env) @@ -175,7 +169,7 @@ c.Assert(err, gc.IsNil) } -func (cs *NewConnSuite) TestConnStateDoesNotUpdateExistingSecrets(c *gc.C) { +func (*NewConnSuite) TestConnStateDoesNotUpdateExistingSecrets(c *gc.C) { attrs := dummy.SampleConfig().Merge(coretesting.Attrs{ "secret": "pork", }) @@ -212,7 +206,7 @@ c.Assert(err, gc.IsNil) } -func (cs *NewConnSuite) TestConnWithPassword(c *gc.C) { +func (*NewConnSuite) TestConnWithPassword(c *gc.C) { attrs := dummy.SampleConfig().Merge(coretesting.Attrs{ "admin-secret": "nutkin", }) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,5 +4,5 @@ APIOpen = &apiOpen APIClose = &apiClose ProviderConnectDelay = &providerConnectDelay - NewAPIFromName = newAPIFromName + NewAPIFromStore = newAPIFromStore ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/package_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,18 +4,21 @@ package osenv_test import ( - "testing" + stdtesting "testing" + "github.com/juju/testing" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing/testbase" ) -func Test(t *testing.T) { +func Test(t *stdtesting.T) { gc.TestingT(t) } -type importSuite struct{} +type importSuite struct { + testing.CleanupSuite +} var _ = gc.Suite(&importSuite{}) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/proxy.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/proxy.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/proxy.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/proxy.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,14 +14,16 @@ http_proxy = "http_proxy" https_proxy = "https_proxy" ftp_proxy = "ftp_proxy" + no_proxy = "no_proxy" ) -// ProxySettings holds the values for the http, https and ftp proxies found by -// Detect Proxies. +// ProxySettings holds the values for the http, https and ftp proxies as well +// as the no_proxy value found by Detect Proxies. type ProxySettings struct { - Http string - Https string - Ftp string + Http string + Https string + Ftp string + NoProxy string } func getProxySetting(key string) string { @@ -35,9 +37,10 @@ // DetectProxies returns the proxy settings found the environment. func DetectProxies() ProxySettings { return ProxySettings{ - Http: getProxySetting(http_proxy), - Https: getProxySetting(https_proxy), - Ftp: getProxySetting(ftp_proxy), + Http: getProxySetting(http_proxy), + Https: getProxySetting(https_proxy), + Ftp: getProxySetting(ftp_proxy), + NoProxy: getProxySetting(no_proxy), } } @@ -57,6 +60,7 @@ addLine(http_proxy, s.Http) addLine(https_proxy, s.Https) addLine(ftp_proxy, s.Ftp) + addLine(no_proxy, s.NoProxy) return strings.Join(lines, "\n") } @@ -76,6 +80,7 @@ addLine(http_proxy, s.Http) addLine(https_proxy, s.Https) addLine(ftp_proxy, s.Ftp) + addLine(no_proxy, s.NoProxy) return lines } @@ -94,4 +99,5 @@ setenv(http_proxy, s.Http) setenv(https_proxy, s.Https) setenv(ftp_proxy, s.Ftp) + setenv(no_proxy, s.NoProxy) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/proxy_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/proxy_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/proxy_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/proxy_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -27,6 +27,8 @@ s.PatchEnvironment("HTTPS_PROXY", "") s.PatchEnvironment("ftp_proxy", "") s.PatchEnvironment("FTP_PROXY", "") + s.PatchEnvironment("no_proxy", "") + s.PatchEnvironment("NO_PROXY", "") proxies := osenv.DetectProxies() @@ -42,13 +44,16 @@ s.PatchEnvironment("HTTPS_PROXY", "") s.PatchEnvironment("ftp_proxy", "ftp://user@10.0.0.1") s.PatchEnvironment("FTP_PROXY", "") + s.PatchEnvironment("no_proxy", "10.0.3.1,localhost") + s.PatchEnvironment("NO_PROXY", "") proxies := osenv.DetectProxies() c.Assert(proxies, gc.DeepEquals, osenv.ProxySettings{ - Http: "http://user@10.0.0.1", - Https: "https://user@10.0.0.1", - Ftp: "ftp://user@10.0.0.1", + Http: "http://user@10.0.0.1", + Https: "https://user@10.0.0.1", + Ftp: "ftp://user@10.0.0.1", + NoProxy: "10.0.3.1,localhost", }) } @@ -61,13 +66,16 @@ s.PatchEnvironment("HTTPS_PROXY", "https://user@10.0.0.2") s.PatchEnvironment("ftp_proxy", "") s.PatchEnvironment("FTP_PROXY", "ftp://user@10.0.0.2") + s.PatchEnvironment("no_proxy", "") + s.PatchEnvironment("NO_PROXY", "10.0.3.1,localhost") proxies := osenv.DetectProxies() c.Assert(proxies, gc.DeepEquals, osenv.ProxySettings{ - Http: "http://user@10.0.0.2", - Https: "https://user@10.0.0.2", - Ftp: "ftp://user@10.0.0.2", + Http: "http://user@10.0.0.2", + Https: "https://user@10.0.0.2", + Ftp: "ftp://user@10.0.0.2", + NoProxy: "10.0.3.1,localhost", }) } @@ -77,16 +85,19 @@ s.PatchEnvironment("http_proxy", "http://user@10.0.0.1") s.PatchEnvironment("https_proxy", "https://user@10.0.0.1") s.PatchEnvironment("ftp_proxy", "ftp://user@10.0.0.1") + s.PatchEnvironment("no_proxy", "10.0.3.1,localhost") s.PatchEnvironment("HTTP_PROXY", "http://user@10.0.0.2") s.PatchEnvironment("HTTPS_PROXY", "https://user@10.0.0.2") s.PatchEnvironment("FTP_PROXY", "ftp://user@10.0.0.2") + s.PatchEnvironment("NO_PROXY", "localhost") proxies := osenv.DetectProxies() c.Assert(proxies, gc.DeepEquals, osenv.ProxySettings{ - Http: "http://user@10.0.0.1", - Https: "https://user@10.0.0.1", - Ftp: "ftp://user@10.0.0.1", + Http: "http://user@10.0.0.1", + Https: "https://user@10.0.0.1", + Ftp: "ftp://user@10.0.0.1", + NoProxy: "10.0.3.1,localhost", }) } @@ -107,9 +118,10 @@ func (s *proxySuite) TestAsScriptEnvironmentAllValue(c *gc.C) { proxies := osenv.ProxySettings{ - Http: "some-value", - Https: "special", - Ftp: "who uses this?", + Http: "some-value", + Https: "special", + Ftp: "who uses this?", + NoProxy: "10.0.3.1,localhost", } expected := ` export http_proxy=some-value @@ -117,7 +129,9 @@ export https_proxy=special export HTTPS_PROXY=special export ftp_proxy=who uses this? -export FTP_PROXY=who uses this?`[1:] +export FTP_PROXY=who uses this? +export no_proxy=10.0.3.1,localhost +export NO_PROXY=10.0.3.1,localhost`[1:] c.Assert(proxies.AsScriptEnvironment(), gc.Equals, expected) } @@ -139,9 +153,10 @@ func (s *proxySuite) TestAsEnvironmentValuesAllValue(c *gc.C) { proxies := osenv.ProxySettings{ - Http: "some-value", - Https: "special", - Ftp: "who uses this?", + Http: "some-value", + Https: "special", + Ftp: "who uses this?", + NoProxy: "10.0.3.1,localhost", } expected := []string{ "http_proxy=some-value", @@ -150,6 +165,8 @@ "HTTPS_PROXY=special", "ftp_proxy=who uses this?", "FTP_PROXY=who uses this?", + "no_proxy=10.0.3.1,localhost", + "NO_PROXY=10.0.3.1,localhost", } c.Assert(proxies.AsEnvironmentValues(), gc.DeepEquals, expected) } @@ -161,11 +178,14 @@ s.PatchEnvironment("HTTPS_PROXY", "initial") s.PatchEnvironment("ftp_proxy", "initial") s.PatchEnvironment("FTP_PROXY", "initial") + s.PatchEnvironment("no_proxy", "initial") + s.PatchEnvironment("NO_PROXY", "initial") proxy := osenv.ProxySettings{ Http: "http proxy", Https: "https proxy", // Ftp left blank to show clearing env. + NoProxy: "10.0.3.1,localhost", } proxy.SetEnvironmentValues() @@ -179,4 +199,6 @@ c.Assert(os.Getenv("HTTPS_PROXY"), gc.Equals, "https proxy") c.Assert(os.Getenv("ftp_proxy"), gc.Equals, "") c.Assert(os.Getenv("FTP_PROXY"), gc.Equals, "") + c.Assert(os.Getenv("no_proxy"), gc.Equals, "10.0.3.1,localhost") + c.Assert(os.Getenv("NO_PROXY"), gc.Equals, "10.0.3.1,localhost") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/vars_linux_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/vars_linux_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/vars_linux_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/vars_linux_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,10 @@ gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/osenv" - "launchpad.net/juju-core/testing/testbase" ) -func (*importSuite) TestHomeLinux(c *gc.C) { +func (s *importSuite) TestHomeLinux(c *gc.C) { h := "/home/foo/bar" - testbase.PatchEnvironment("HOME", h) + s.PatchEnvironment("HOME", h) c.Check(osenv.Home(), gc.Equals, h) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/vars_windows_test.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/vars_windows_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/osenv/vars_windows_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/osenv/vars_windows_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,12 +6,11 @@ gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/osenv" - "launchpad.net/juju-core/testing" ) -func (*importSuite) TestHome(c *gc.C) { - testbase.PatchEnvironment("HOMEPATH", "") - testbase.PatchEnvironment("HOMEDRIVE", "") +func (s *importSuite) TestHome(c *gc.C) { + s.PatchEnvironment("HOMEPATH", "") + s.PatchEnvironment("HOMEDRIVE", "") drive := "P:" path := `\home\foo\bar` diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/conn.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/conn.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/conn.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/conn.go 2014-03-20 12:52:38.000000000 +0000 @@ -62,6 +62,7 @@ oldHome string oldJujuHome string environ environs.Environ + DummyConfig testing.Attrs } const AdminSecret = "dummy-secret" @@ -220,7 +221,10 @@ } func (s *JujuConnSuite) writeSampleConfig(c *gc.C, path string) { - attrs := dummy.SampleConfig().Merge(testing.Attrs{ + if s.DummyConfig == nil { + s.DummyConfig = dummy.SampleConfig() + } + attrs := s.DummyConfig.Merge(testing.Attrs{ "admin-secret": AdminSecret, "agent-version": version.Current.Number.String(), }).Delete("name") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/instance.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/instance.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/instance.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/instance.go 2014-03-20 12:52:38.000000000 +0000 @@ -85,6 +85,31 @@ ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { + return StartInstanceWithConstraintsAndNetworks(env, machineId, cons, environs.Networks{}) +} + +// AssertStartInstanceWithNetworks is a test helper function that starts an +// instance with the given networks, and a plausible but invalid +// configuration, and returns the result of Environ.StartInstance. +func AssertStartInstanceWithNetworks( + c *gc.C, env environs.Environ, machineId string, cons constraints.Value, nets environs.Networks, +) ( + instance.Instance, *instance.HardwareCharacteristics, +) { + inst, hc, err := StartInstanceWithConstraintsAndNetworks(env, machineId, cons, nets) + c.Assert(err, gc.IsNil) + return inst, hc +} + +// StartInstanceWithNetworks is a test helper function that starts an instance +// with the given networks, and a plausible but invalid configuration, and +// returns the result of Environ.StartInstance. +func StartInstanceWithConstraintsAndNetworks( + env environs.Environ, machineId string, cons constraints.Value, + nets environs.Networks, +) ( + instance.Instance, *instance.HardwareCharacteristics, error, +) { series := env.Config().DefaultSeries() agentVersion, ok := env.Config().AgentVersion() if !ok { @@ -98,5 +123,10 @@ stateInfo := FakeStateInfo(machineId) apiInfo := FakeAPIInfo(machineId) machineConfig := environs.NewMachineConfig(machineId, machineNonce, stateInfo, apiInfo) - return env.StartInstance(cons, possibleTools, machineConfig) + return env.StartInstance(environs.StartInstanceParams{ + Constraints: cons, + Networks: nets, + Tools: possibleTools, + MachineConfig: machineConfig, + }) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/repo.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/repo.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/repo.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/repo.go 2014-03-20 12:52:38.000000000 +0000 @@ -25,11 +25,8 @@ s.JujuConnSuite.SetUpTest(c) // Change the environ's config to ensure we're using the one in state, // not the one in the local environments.yaml - oldcfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - cfg, err := oldcfg.Apply(map[string]interface{}{"default-series": "precise"}) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) + updateAttrs := map[string]interface{}{"default-series": "precise"} + err := s.State.UpdateEnvironConfig(updateAttrs, nil, nil) c.Assert(err, gc.IsNil) s.RepoPath = os.Getenv("JUJU_REPOSITORY") repoPath := c.MkDir() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/utils.go juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/utils.go --- juju-core-1.17.4/src/launchpad.net/juju-core/juju/testing/utils.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/utils.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,24 +6,10 @@ import ( gc "launchpad.net/gocheck" - "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" - coretesting "launchpad.net/juju-core/testing" ) -// ChangeEnvironConfig applies the given change function -// to the attributes from st.EnvironConfig and -// sets the state's environment configuration to the result. -func ChangeEnvironConfig(c *gc.C, st *state.State, change func(coretesting.Attrs) coretesting.Attrs) { - cfg, err := st.EnvironConfig() - c.Assert(err, gc.IsNil) - newCfg, err := config.New(config.NoDefaults, change(cfg.AllAttrs())) - c.Assert(err, gc.IsNil) - err = st.SetEnvironConfig(newCfg, cfg) - c.Assert(err, gc.IsNil) -} - // AddStateServerMachine adds a "state server" machine to the state so // that State.Addresses and State.APIAddresses will work. It returns the // added machine. The addresses that those methods will return bear no diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/log/log.go juju-core-1.17.6/src/launchpad.net/juju-core/log/log.go --- juju-core-1.17.4/src/launchpad.net/juju-core/log/log.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/log/log.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "fmt" - "github.com/loggo/loggo" + "github.com/juju/loggo" ) var ( diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/log/log_test.go juju-core-1.17.6/src/launchpad.net/juju-core/log/log_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/log/log_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/log/log_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,7 +9,7 @@ "testing" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/log" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/config.go juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -22,7 +22,7 @@ // // The apparmor profile is quite strict about where rsyslog can write files. // Instead of poking with the profile, the local provider now logs to -// /var/log/juju-{{user}}-{{env name}}/all-machines.log, and a symlink is made +// {{logDir}}-{{user}}-{{env name}}/all-machines.log, and a symlink is made // in the local provider log dir to point to that file. The file is also // created with 0644 so the user can read it without poking permissions. By // default rsyslog creates files with 0644, but in the ubuntu package, the @@ -35,7 +35,7 @@ // // if $syslogtag startswith "juju{{namespace}}-" then // action(type="omfile" -// File="{{logDir}}/all-machines.log" +// File="{{logDir}}{{namespace}}/all-machines.log" // Template="JujuLogFormat{{namespace}}" // FileCreateMode="0644") // & stop @@ -147,13 +147,13 @@ // NewForwardConfig creates a SyslogConfig instance used on unit nodes to forward log entries // to the state server nodes. -func NewForwardConfig(logFile string, port int, namespace string, stateServerAddresses []string) *SyslogConfig { +func NewForwardConfig(logFile, logDir string, port int, namespace string, stateServerAddresses []string) *SyslogConfig { conf := &SyslogConfig{ configTemplate: nodeRsyslogTemplate, StateServerAddresses: stateServerAddresses, LogFileName: logFile, Port: port, - LogDir: "/var/log/juju", + LogDir: logDir, } if namespace != "" { conf.Namespace = "-" + namespace @@ -163,12 +163,12 @@ // NewAccumulateConfig creates a SyslogConfig instance used to accumulate log entries from the // various unit nodes. -func NewAccumulateConfig(logFile string, port int, namespace string) *SyslogConfig { +func NewAccumulateConfig(logFile, logDir string, port int, namespace string) *SyslogConfig { conf := &SyslogConfig{ configTemplate: stateServerRsyslogTemplate, LogFileName: logFile, Port: port, - LogDir: "/var/log/juju", + LogDir: logDir, } if namespace != "" { conf.Namespace = "-" + namespace diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,7 @@ gc "launchpad.net/gocheck" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/log/syslog" syslogtesting "launchpad.net/juju-core/log/syslog/testing" ) @@ -42,13 +43,16 @@ } func (s *SyslogConfigSuite) TestAccumulateConfigRender(c *gc.C) { - syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", 8888, "") + syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", agent.DefaultLogDir, 8888, "") s.assertRsyslogConfigContents( - c, syslogConfigRenderer, syslogtesting.ExpectedAccumulateSyslogConf(c, "some-machine", "", 8888)) + c, + syslogConfigRenderer, + syslogtesting.ExpectedAccumulateSyslogConf(c, "some-machine", "", 8888), + ) } func (s *SyslogConfigSuite) TestAccumulateConfigWrite(c *gc.C) { - syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", 8888, "") + syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", agent.DefaultLogDir, 8888, "") syslogConfigRenderer.ConfigDir = s.configDir syslogConfigRenderer.ConfigFileName = "rsyslog.conf" s.assertRsyslogConfigPath(c, syslogConfigRenderer) @@ -56,30 +60,49 @@ c.Assert(err, gc.IsNil) syslogConfData, err := ioutil.ReadFile(syslogConfigRenderer.ConfigFilePath()) c.Assert(err, gc.IsNil) - c.Assert(string(syslogConfData), gc.Equals, syslogtesting.ExpectedAccumulateSyslogConf(c, "some-machine", "", 8888)) + c.Assert( + string(syslogConfData), + gc.Equals, + syslogtesting.ExpectedAccumulateSyslogConf(c, "some-machine", "", 8888), + ) } func (s *SyslogConfigSuite) TestAccumulateConfigRenderWithNamespace(c *gc.C) { - syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", 8888, "namespace") + syslogConfigRenderer := syslog.NewAccumulateConfig("some-machine", agent.DefaultLogDir, 8888, "namespace") syslogConfigRenderer.LogDir += "-namespace" s.assertRsyslogConfigContents( - c, syslogConfigRenderer, syslogtesting.ExpectedAccumulateSyslogConf(c, "some-machine", "namespace", 8888)) + c, syslogConfigRenderer, syslogtesting.ExpectedAccumulateSyslogConf( + c, "some-machine", "namespace", 8888, + ), + ) } func (s *SyslogConfigSuite) TestForwardConfigRender(c *gc.C) { - syslogConfigRenderer := syslog.NewForwardConfig("some-machine", 999, "", []string{"server"}) + syslogConfigRenderer := syslog.NewForwardConfig( + "some-machine", agent.DefaultLogDir, 999, "", []string{"server"}, + ) s.assertRsyslogConfigContents( - c, syslogConfigRenderer, syslogtesting.ExpectedForwardSyslogConf(c, "some-machine", "", "server", 999)) + c, syslogConfigRenderer, syslogtesting.ExpectedForwardSyslogConf( + c, "some-machine", agent.DefaultLogDir, "", "server", 999, + ), + ) } func (s *SyslogConfigSuite) TestForwardConfigRenderWithNamespace(c *gc.C) { - syslogConfigRenderer := syslog.NewForwardConfig("some-machine", 999, "namespace", []string{"server"}) + syslogConfigRenderer := syslog.NewForwardConfig( + "some-machine", agent.DefaultLogDir, 999, "namespace", []string{"server"}, + ) s.assertRsyslogConfigContents( - c, syslogConfigRenderer, syslogtesting.ExpectedForwardSyslogConf(c, "some-machine", "namespace", "server", 999)) + c, syslogConfigRenderer, syslogtesting.ExpectedForwardSyslogConf( + c, "some-machine", agent.DefaultLogDir, "namespace", "server", 999, + ), + ) } func (s *SyslogConfigSuite) TestForwardConfigWrite(c *gc.C) { - syslogConfigRenderer := syslog.NewForwardConfig("some-machine", 999, "", []string{"server"}) + syslogConfigRenderer := syslog.NewForwardConfig( + "some-machine", agent.DefaultLogDir, 999, "", []string{"server"}, + ) syslogConfigRenderer.ConfigDir = s.configDir syslogConfigRenderer.ConfigFileName = "rsyslog.conf" s.assertRsyslogConfigPath(c, syslogConfigRenderer) @@ -88,5 +111,10 @@ syslogConfData, err := ioutil.ReadFile(syslogConfigRenderer.ConfigFilePath()) c.Assert(err, gc.IsNil) c.Assert( - string(syslogConfData), gc.Equals, syslogtesting.ExpectedForwardSyslogConf(c, "some-machine", "", "server", 999)) + string(syslogConfData), + gc.Equals, + syslogtesting.ExpectedForwardSyslogConf( + c, "some-machine", agent.DefaultLogDir, "", "server", 999, + ), + ) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/testing/syslogconf.go juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/testing/syslogconf.go --- juju-core-1.17.4/src/launchpad.net/juju-core/log/syslog/testing/syslogconf.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/log/syslog/testing/syslogconf.go 2014-03-20 12:52:38.000000000 +0000 @@ -41,6 +41,7 @@ type templateArgs struct { MachineTag string + LogDir string Namespace string BootstrapIP string Port int @@ -75,13 +76,13 @@ $InputFilePersistStateInterval 50 $InputFilePollInterval 5 -$InputFileName /var/log/juju/{{.MachineTag}}.log +$InputFileName {{.LogDir}}/{{.MachineTag}}.log $InputFileTag juju{{.Namespace}}-{{.MachineTag}}: $InputFileStateFile {{.MachineTag}}{{.Namespace}} $InputRunFileMonitor $DefaultNetstreamDriver gtls -$DefaultNetstreamDriverCAFile /var/log/juju/ca-cert.pem +$DefaultNetstreamDriverCAFile {{.LogDir}}/ca-cert.pem $ActionSendStreamDriverAuthMode anon $ActionSendStreamDriverMode 1 # run driver in TLS-only mode @@ -92,7 +93,7 @@ ` // ExpectedForwardSyslogConf returns the expected content for a rsyslog file on a host machine. -func ExpectedForwardSyslogConf(c *gc.C, machineTag, namespace, bootstrapIP string, port int) string { +func ExpectedForwardSyslogConf(c *gc.C, machineTag, logDir, namespace, bootstrapIP string, port int) string { if namespace != "" { namespace = "-" + namespace } @@ -100,6 +101,7 @@ var conf bytes.Buffer err := t.Execute(&conf, templateArgs{ MachineTag: machineTag, + LogDir: logDir, Namespace: namespace, BootstrapIP: bootstrapIP, Port: port, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/Makefile juju-core-1.17.6/src/launchpad.net/juju-core/Makefile --- juju-core-1.17.4/src/launchpad.net/juju-core/Makefile 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/Makefile 2014-03-20 12:52:38.000000000 +0000 @@ -9,15 +9,23 @@ PROJECT := launchpad.net/juju-core PROJECT_DIR := $(shell go list -e -f '{{.Dir}}' $(PROJECT)) +ifeq ($(shell uname -p | sed -r 's/.*(x86|armel|armhf).*/golang/'), golang) + GO_C := golang + INSTALL_FLAGS := +else + GO_C := gccgo-4.9 gccgo-go + INSTALL_FLAGS := -gccgoflags=-static-libgo +endif + define DEPENDENCIES build-essential bzr distro-info-data git-core - golang mercurial - mongodb-server + rsyslog-gnutls zip + $(GO_C) endef default: build @@ -33,7 +41,7 @@ go test $(PROJECT)/... install: - go install -v $(PROJECT)/... + go install $(INSTALL_FLAGS) -v $(PROJECT)/... clean: go clean $(PROJECT)/... @@ -74,8 +82,13 @@ @sudo apt-get update endif @echo Installing dependencies - @sudo apt-get --yes install $(strip $(DEPENDENCIES)) + @sudo apt-get --yes install $(strip $(DEPENDENCIES)) \ + $(shell apt-cache madison juju-mongodb mongodb-server | head -1 | cut -d '|' -f1) +# Install bash_completion +install-etc: + @echo Installing bash completion + @sudo install -o root -g root -m 644 etc/bash_completion.d/juju-core /etc/bash_completion.d .PHONY: build check install .PHONY: clean format simplify diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/names/user_test.go juju-core-1.17.6/src/launchpad.net/juju-core/names/user_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/names/user_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/names/user_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,10 +4,10 @@ package names_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/names" - jc "launchpad.net/juju-core/testing/checkers" ) type userSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/certfile_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/certfile_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/certfile_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/certfile_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,9 +7,8 @@ "io/ioutil" "os" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" ) type certFileSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/config.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -95,32 +95,40 @@ return cfg.Apply(envCfg.attrs) } -const boilerplateYAML = ` +var boilerplateYAML = ` # https://juju.ubuntu.com/docs/config-azure.html azure: type: azure - # location specifies the place where instances will be started, for - # example: West US, North Europe. + # location specifies the place where instances will be started, + # for example: West US, North Europe. + # location: West US - - # The following attributes specify Windows Azure Management information. - # See http://msdn.microsoft.com/en-us/library/windowsazure + + # The following attributes specify Windows Azure Management + # information. See: + # http://msdn.microsoft.com/en-us/library/windowsazure # for details. + # management-subscription-id: <00000000-0000-0000-0000-000000000000> management-certificate-path: /home/me/azure.pem # storage-account-name holds Windows Azure Storage info. + # storage-account-name: abcdefghijkl - # force-image-name overrides the OS image selection to use - # a fixed image for all deployments. Most useful for developers. + # force-image-name overrides the OS image selection to use a fixed + # image for all deployments. Most useful for developers. + # # force-image-name: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-13_10-amd64-server-DEVELOPMENT-20130713-Juju_ALPHA-en-us-30GB - # image-stream chooses a simplestreams stream to select OS images from, - # for example daily or released images (or any other stream available on simplestreams). + # image-stream chooses a simplestreams stream to select OS images + # from, for example daily or released images (or any other stream + # available on simplestreams). + # # image-stream: "released" -` + +`[1:] func (prov azureEnvironProvider) BoilerplateConfig() string { return boilerplateYAML diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/customdata_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/customdata_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,11 +8,13 @@ gc "launchpad.net/gocheck" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" @@ -31,7 +33,8 @@ MachineId: machineID, MachineNonce: "gxshasqlnng", DataDir: environs.DataDir, - LogDir: environs.LogDir, + LogDir: agent.DefaultLogDir, + Jobs: []params.MachineJob{params.JobManageEnviron, params.JobHostUnits}, CloudInitOutputLog: environs.CloudInitOutputLog, Tools: &tools.Tools{URL: "file://" + c.MkDir()}, StateInfo: &state.Info{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,6 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/instances" @@ -24,7 +23,6 @@ "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" - "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils/parallel" ) @@ -59,6 +57,12 @@ // name is immutable; it does not need locking. name string + // archMutex gates access to supportedArchitectures + archMutex sync.Mutex + // supportedArchitectures caches the architectures + // for which images can be instantiated. + supportedArchitectures []string + // ecfg is the environment's Azure-specific configuration. ecfg *azureEnvironConfig @@ -303,9 +307,6 @@ return req, nil } -// architectures lists the CPU architectures supported by Azure. -var architectures = []string{"amd64", "i386"} - // newHostedService creates a hosted service. It will make up a unique name, // starting with the given prefix. func newHostedService(azure *gwacl.ManagementAPI, prefix string, affinityGroupName string, location string) (*gwacl.CreateHostedService, error) { @@ -323,9 +324,32 @@ return svc, nil } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (env *azureEnviron) SupportedArchitectures() ([]string, error) { + env.archMutex.Lock() + defer env.archMutex.Unlock() + if env.supportedArchitectures != nil { + return env.supportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + ecfg := env.getSnapshot().ecfg + region := ecfg.location() + cloudSpec := simplestreams.CloudSpec{ + Region: region, + Endpoint: getEndpoint(region), + } + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + Stream: ecfg.ImageStream(), + }) + var err error + env.supportedArchitectures, err = common.SupportedArchitectures(env, imageConstraint) + return env.supportedArchitectures, err +} + // selectInstanceTypeAndImage returns the appropriate instance-type name and // the OS image name for launching a virtual machine with the given parameters. -func (env *azureEnviron) selectInstanceTypeAndImage(cons constraints.Value, series, location string) (string, string, error) { +func (env *azureEnviron) selectInstanceTypeAndImage(constraint *instances.InstanceConstraint) (string, string, error) { ecfg := env.getSnapshot().ecfg sourceImageName := ecfg.forceImageName() if sourceImageName != "" { @@ -337,24 +361,14 @@ // instance type either. // // Select the instance type using simple, Azure-specific code. - machineType, err := selectMachineType(gwacl.RoleSizes, defaultToBaselineSpec(cons)) + machineType, err := selectMachineType(gwacl.RoleSizes, defaultToBaselineSpec(constraint.Constraints)) if err != nil { return "", "", err } return machineType.Name, sourceImageName, nil } - // Choose the most suitable instance type and OS image, based on - // simplestreams information. - // - // This should be the normal execution path. The user is not expected - // to configure a source image name in normal use. - constraint := instances.InstanceConstraint{ - Region: location, - Series: series, - Arches: architectures, - Constraints: cons, - } + // Choose the most suitable instance type and OS image, based on simplestreams information. spec, err := findInstanceSpec(env, constraint) if err != nil { return "", "", err @@ -363,24 +377,23 @@ } // StartInstance is specified in the InstanceBroker interface. -func (env *azureEnviron) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (_ instance.Instance, _ *instance.HardwareCharacteristics, err error) { +func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (_ instance.Instance, _ *instance.HardwareCharacteristics, err error) { // Declaring "err" in the function signature so that we can "defer" // any cleanup that needs to run during error returns. - err = environs.FinishMachineConfig(machineConfig, env.Config(), cons) + err = environs.FinishMachineConfig(args.MachineConfig, env.Config(), args.Constraints) if err != nil { return nil, nil, err } // Pick envtools. Needed for the custom data (which is what we normally // call userdata). - machineConfig.Tools = possibleTools[0] - logger.Infof("picked tools %q", machineConfig.Tools) + args.MachineConfig.Tools = args.Tools[0] + logger.Infof("picked tools %q", args.MachineConfig.Tools) // Compose userdata. - userData, err := makeCustomData(machineConfig) + userData, err := makeCustomData(args.MachineConfig) if err != nil { return nil, nil, fmt.Errorf("custom data: %v", err) } @@ -409,8 +422,12 @@ } }() - series := possibleTools.OneSeries() - instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(cons, series, location) + instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ + Region: location, + Series: args.Tools.OneSeries(), + Arches: args.Tools.Arches(), + Constraints: args.Constraints, + }) if err != nil { return nil, nil, err } @@ -860,9 +877,9 @@ // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. func (env *azureEnviron) GetImageSources() ([]simplestreams.DataSource, error) { sources := make([]simplestreams.DataSource, 1+len(baseURLs)) - sources[0] = storage.NewStorageSimpleStreamsDataSource(env.Storage(), storage.BaseImagesPath) + sources[0] = storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseImagesPath) for i, url := range baseURLs { - sources[i+1] = simplestreams.NewURLDataSource(url, simplestreams.VerifySSLHostnames) + sources[i+1] = simplestreams.NewURLDataSource("Azure base URL", url, simplestreams.VerifySSLHostnames) } return sources, nil } @@ -871,7 +888,7 @@ func (env *azureEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. sources := []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(env.Storage(), storage.BaseToolsPath)} + storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseToolsPath)} return sources, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environprovider.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environprovider.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environprovider.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environprovider.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "fmt" "io/ioutil" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environ_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/environ_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,6 +16,7 @@ "strings" "sync" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/gwacl" @@ -24,13 +25,13 @@ "launchpad.net/juju-core/environs/bootstrap" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" + "launchpad.net/juju-core/environs/instances" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type environSuite struct { @@ -65,6 +66,17 @@ s.AddCleanup(func(c *gc.C) { closer.Close() }) } +func (*environSuite) TestGetEndpoint(c *gc.C) { + c.Check( + getEndpoint("West US"), + gc.Equals, + "https://management.core.windows.net/") + c.Check( + getEndpoint("China East"), + gc.Equals, + "https://management.core.chinacloudapi.cn/") +} + func (*environSuite) TestGetSnapshot(c *gc.C) { original := azureEnviron{name: "this-env", ecfg: new(azureEnvironConfig)} snapshot := original.getSnapshot() @@ -163,6 +175,13 @@ return gwacl.PatchManagementAPIResponses(responses) } +func (s *environSuite) TestSupportedArchitectures(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) + a, err := env.SupportedArchitectures() + c.Assert(err, gc.IsNil) + c.Assert(a, gc.DeepEquals, []string{"ppc64"}) +} + func (suite *environSuite) TestGetEnvPrefixContainsEnvName(c *gc.C) { env := makeEnviron(c) c.Check(strings.Contains(env.getEnvPrefix(), env.Name()), jc.IsTrue) @@ -1162,42 +1181,53 @@ Mem: &aim.Mem, } - instanceType, image, err := env.selectInstanceTypeAndImage(cons, "precise", "West US") + instanceType, image, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ + Region: "West US", + Series: "precise", + Constraints: cons, + }) c.Assert(err, gc.IsNil) c.Check(instanceType, gc.Equals, aim.Name) c.Check(image, gc.Equals, forcedImage) } -func (*environSuite) TestSelectInstanceTypeAndImageUsesSimplestreamsByDefault(c *gc.C) { - env := makeEnviron(c) +func (s *environSuite) setupEnvWithDummyMetadata(c *gc.C) *azureEnviron { + envAttrs := makeAzureConfigMap(c) + envAttrs["location"] = "North Europe" + env := makeEnvironWithConfig(c, envAttrs) + s.setDummyStorage(c, env) + s.PatchValue(&imagemetadata.DefaultBaseURL, "") + s.PatchValue(&signedImageDataOnly, false) + images := []*imagemetadata.ImageMetadata{ + { + Id: "image-id", + VirtType: "Hyper-V", + Arch: "ppc64", + RegionName: "North Europe", + Endpoint: "https://management.core.windows.net/", + }, + } + makeTestMetadata(c, env, "precise", "North Europe", images) + return env +} +func (s *environSuite) TestSelectInstanceTypeAndImageUsesSimplestreamsByDefault(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) // We'll tailor our constraints so as to get a specific instance type. aim := gwacl.RoleNameMap["ExtraSmall"] cons := constraints.Value{ CpuCores: &aim.CpuCores, Mem: &aim.Mem, } - - // We have one image available. - images := []*imagemetadata.ImageMetadata{ - { - Id: "image", - VType: "Hyper-V", - Arch: "amd64", - RegionAlias: "North Europe", - RegionName: "North Europe", - Endpoint: "http://localhost/", - }, - } - cleanup := patchFetchImageMetadata(images, nil) - defer cleanup() - - instanceType, image, err := env.selectInstanceTypeAndImage(cons, "precise", "West US") + instanceType, image, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ + Region: "North Europe", + Series: "precise", + Constraints: cons, + }) c.Assert(err, gc.IsNil) - - c.Check(instanceType, gc.Equals, aim.Name) - c.Check(image, gc.Equals, "image") + c.Assert(instanceType, gc.Equals, aim.Name) + c.Assert(image, gc.Equals, "image-id") } func (*environSuite) TestConvertToInstances(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instance_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instance_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instance_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instance_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,11 +8,11 @@ "fmt" "net/http" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/gwacl" "launchpad.net/juju-core/instance" - jc "launchpad.net/juju-core/testing/checkers" ) type instanceSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instancetype.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instancetype.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instancetype.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instancetype.go 2014-03-20 12:52:38.000000000 +0000 @@ -121,10 +121,6 @@ // this setting. var signedImageDataOnly = true -// fetchImageMetadata is a pointer to the imagemetadata.Fetch function. It's -// only needed as an injection point where tests can substitute fakes. -var fetchImageMetadata = imagemetadata.Fetch - // findMatchingImages queries simplestreams for OS images that match the given // requirements. // @@ -142,7 +138,7 @@ return nil, err } indexPath := simplestreams.DefaultIndexPath - images, err := fetchImageMetadata(sources, indexPath, constraint, signedImageDataOnly) + images, _, err := imagemetadata.Fetch(sources, indexPath, constraint, signedImageDataOnly) if len(images) == 0 || errors.IsNotFoundError(err) { return nil, fmt.Errorf("no OS images found for location %q, series %q, architectures %q (and endpoint: %q)", location, series, arches, endpoint) } else if err != nil { @@ -161,12 +157,11 @@ return instances.InstanceType{ Id: roleSize.Name, Name: roleSize.Name, - Arches: architectures, CpuCores: roleSize.CpuCores, Mem: roleSize.Mem, RootDisk: roleSize.OSDiskSpaceVirt, Cost: roleSize.Cost, - VType: &vtype, + VirtType: &vtype, CpuPower: &cpuPower, // tags are not currently supported by azure } @@ -174,23 +169,31 @@ // listInstanceTypes describes the available instance types based on a // description in gwacl's terms. -func listInstanceTypes(roleSizes []gwacl.RoleSize) []instances.InstanceType { +func listInstanceTypes(env *azureEnviron, roleSizes []gwacl.RoleSize) ([]instances.InstanceType, error) { + arches, err := env.SupportedArchitectures() + if err != nil { + return nil, err + } types := make([]instances.InstanceType, len(roleSizes)) for index, roleSize := range roleSizes { types[index] = newInstanceType(roleSize) + types[index].Arches = arches } - return types + return types, nil } // findInstanceSpec returns the InstanceSpec that best satisfies the supplied // InstanceConstraint. -func findInstanceSpec(env *azureEnviron, constraint instances.InstanceConstraint) (*instances.InstanceSpec, error) { +func findInstanceSpec(env *azureEnviron, constraint *instances.InstanceConstraint) (*instances.InstanceSpec, error) { constraint.Constraints = defaultToBaselineSpec(constraint.Constraints) imageData, err := findMatchingImages(env, constraint.Region, constraint.Series, constraint.Arches) if err != nil { return nil, err } images := instances.ImageMetadataToImages(imageData) - instanceTypes := listInstanceTypes(gwacl.RoleSizes) - return instances.FindInstanceSpec(images, &constraint, instanceTypes) + instanceTypes, err := listInstanceTypes(env, gwacl.RoleSizes) + if err != nil { + return nil, err + } + return instances.FindInstanceSpec(images, constraint, instanceTypes) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instancetype_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instancetype_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/instancetype_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/instancetype_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,22 +4,38 @@ package azure import ( - "fmt" - gc "launchpad.net/gocheck" "launchpad.net/gwacl" "launchpad.net/juju-core/constraints" + "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/instances" - "launchpad.net/juju-core/environs/jujutest" "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/environs/testing" ) -type instanceTypeSuite struct{} +type instanceTypeSuite struct { + providerSuite +} var _ = gc.Suite(&instanceTypeSuite{}) +func (s *instanceTypeSuite) SetUpTest(c *gc.C) { + s.providerSuite.SetUpTest(c) + s.PatchValue(&imagemetadata.DefaultBaseURL, "") + s.PatchValue(&signedImageDataOnly, false) +} + +// setDummyStorage injects the local provider's fake storage implementation +// into the given environment, so that tests can manipulate storage as if it +// were real. +func (s *instanceTypeSuite) setDummyStorage(c *gc.C, env *azureEnviron) { + closer, storage, _ := testing.CreateLocalTestStorage(c) + env.storage = storage + s.AddCleanup(func(c *gc.C) { closer.Close() }) +} + func (*instanceTypeSuite) TestNewPreferredTypesAcceptsNil(c *gc.C) { types := newPreferredTypes(nil) @@ -214,209 +230,60 @@ c.Check(choice.Name, gc.Equals, "Lambo") } -// fakeSimpleStreamsScheme is a fake protocol which tests can use for their -// simplestreams base URLs. -const fakeSimpleStreamsScheme = "azure-simplestreams-test" - -// testRoundTripper is a fake http-like transport for injecting fake -// simplestream responses into these tests. -var testRoundTripper = jujutest.ProxyRoundTripper{} - -func init() { - // Route any request for a URL on the fakeSimpleStreamsScheme protocol - // to testRoundTripper. - testRoundTripper.RegisterForScheme(fakeSimpleStreamsScheme) -} - -// prepareSimpleStreamsResponse sets up a fake response for our query to -// SimpleStreams. -// -// It returns a cleanup function, which you must call to reset things when -// done. -func prepareSimpleStreamsResponse(stream, location, series, release, arch, json string) func() { - fakeURL := fakeSimpleStreamsScheme + "://" - originalAzureURLs := baseURLs - originalDefaultURL := imagemetadata.DefaultBaseURL - baseURLs = []string{fakeURL} - imagemetadata.DefaultBaseURL = "" - - originalSignedOnly := signedImageDataOnly - signedImageDataOnly = false - - azureName := fmt.Sprintf("com.ubuntu.cloud:%s:azure", stream) - streamSuffix := "" - if stream != "released" { - streamSuffix = "." + stream - } - - // Generate an index. It will point to an Azure index with the - // caller's json. - index := fmt.Sprintf(` +func (s *instanceTypeSuite) setupEnvWithDummyMetadata(c *gc.C) *azureEnviron { + envAttrs := makeAzureConfigMap(c) + envAttrs["location"] = "West US" + env := makeEnvironWithConfig(c, envAttrs) + s.setDummyStorage(c, env) + images := []*imagemetadata.ImageMetadata{ { - "index": { - %q: { - "updated": "Thu, 08 Aug 2013 07:55:58 +0000", - "clouds": [ - { - "region": %q, - "endpoint": "https://management.core.windows.net/" - } - ], - "format": "products:1.0", - "datatype": "image-ids", - "cloudname": "azure", - "products": [ - "com.ubuntu.cloud%s:server:%s:%s" - ], - "path": "/v1/%s.json" - } - }, - "updated": "Thu, 08 Aug 2013 07:55:58 +0000", - "format": "index:1.0" - } - `, azureName, location, streamSuffix, release, arch, azureName) - files := map[string]string{ - "/v1/index.json": index, - "/v1/" + azureName + ".json": json, - } - testRoundTripper.Sub = jujutest.NewCannedRoundTripper(files, nil) - return func() { - baseURLs = originalAzureURLs - imagemetadata.DefaultBaseURL = originalDefaultURL - signedImageDataOnly = originalSignedOnly - testRoundTripper.Sub = nil + Id: "image-id", + VirtType: "Hyper-V", + Arch: "amd64", + RegionName: "West US", + Endpoint: "https://management.core.windows.net/", + }, } + makeTestMetadata(c, env, "precise", "West US", images) + return env } -func (*environSuite) TestGetEndpoint(c *gc.C) { - c.Check( - getEndpoint("West US"), - gc.Equals, - "https://management.core.windows.net/") - c.Check( - getEndpoint("China East"), - gc.Equals, - "https://management.core.chinacloudapi.cn/") -} - -func (*instanceTypeSuite) TestFindMatchingImagesReturnsErrorIfNoneFound(c *gc.C) { - emptyResponse := ` - { - "format": "products:1.0" - } - ` - cleanup := prepareSimpleStreamsResponse("released", "West US", "precise", "12.04", "amd64", emptyResponse) - defer cleanup() - - env := makeEnviron(c) +func (s *instanceTypeSuite) TestFindMatchingImagesReturnsErrorIfNoneFound(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) _, err := findMatchingImages(env, "West US", "saucy", []string{"amd64"}) c.Assert(err, gc.NotNil) - - c.Check(err, gc.ErrorMatches, "no OS images found for location .*") + c.Assert(err, gc.ErrorMatches, "no OS images found for location .*") } -func (*instanceTypeSuite) TestFindMatchingImagesReturnsReleasedImages(c *gc.C) { - // Based on real-world simplestreams data, pared down to a minimum: - response := ` - { - "updated": "Tue, 09 Jul 2013 22:35:10 +0000", - "datatype": "image-ids", - "content_id": "com.ubuntu.cloud:released", - "products": { - "com.ubuntu.cloud:server:12.04:amd64": { - "release": "precise", - "version": "12.04", - "arch": "amd64", - "versions": { - "20130603": { - "items": { - "euww1i3": { - "virt": "Hyper-V", - "crsn": "West Europe", - "root_size": "30GB", - "id": "MATCHING-IMAGE" - } - }, - "pub_name": "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-12_04_2-LTS-amd64-server-20130603-en-us-30GB", - "publabel": "Ubuntu Server 12.04.2 LTS", - "label": "release" - } - } - } - }, - "format": "products:1.0", - "_aliases": { - "crsn": { - "West Europe": { - "region": "West Europe", - "endpoint": "https://management.core.windows.net/" - } - } - } - } - ` - cleanup := prepareSimpleStreamsResponse("released", "West Europe", "precise", "12.04", "amd64", response) - defer cleanup() - - env := makeEnviron(c) - images, err := findMatchingImages(env, "West Europe", "precise", []string{"amd64"}) +func (s *instanceTypeSuite) TestFindMatchingImagesReturnsReleasedImages(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) + images, err := findMatchingImages(env, "West US", "precise", []string{"amd64"}) c.Assert(err, gc.IsNil) - c.Assert(images, gc.HasLen, 1) - c.Check(images[0].Id, gc.Equals, "MATCHING-IMAGE") + c.Check(images[0].Id, gc.Equals, "image-id") } -func (*instanceTypeSuite) TestFindMatchingImagesReturnsDailyImages(c *gc.C) { - // Based on real-world simplestreams data, pared down to a minimum: - response := ` - { - "updated": "Tue, 09 Jul 2013 22:35:10 +0000", - "datatype": "image-ids", - "content_id": "com.ubuntu.cloud:daily:azure", - "products": { - "com.ubuntu.cloud.daily:server:12.04:amd64": { - "release": "precise", - "version": "12.04", - "arch": "amd64", - "versions": { - "20130603": { - "items": { - "euww1i3": { - "virt": "Hyper-V", - "crsn": "West Europe", - "root_size": "30GB", - "id": "MATCHING-IMAGE" - } - }, - "pub_name": "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-12_04_2-LTS-amd64-server-20130603-en-us-30GB", - "publabel": "Ubuntu Server 12.04.2 LTS", - "label": "release" - } - } - } - }, - "format": "products:1.0", - "_aliases": { - "crsn": { - "West Europe": { - "region": "West Europe", - "endpoint": "https://management.core.windows.net/" - } - } - } - } - ` - cleanup := prepareSimpleStreamsResponse("daily", "West Europe", "precise", "12.04", "amd64", response) - defer cleanup() - +func (s *instanceTypeSuite) TestFindMatchingImagesReturnsDailyImages(c *gc.C) { envAttrs := makeAzureConfigMap(c) envAttrs["image-stream"] = "daily" + envAttrs["location"] = "West US" env := makeEnvironWithConfig(c, envAttrs) - images, err := findMatchingImages(env, "West Europe", "precise", []string{"amd64"}) + s.setDummyStorage(c, env) + images := []*imagemetadata.ImageMetadata{ + { + Id: "image-id", + VirtType: "Hyper-V", + Arch: "amd64", + RegionName: "West US", + Endpoint: "https://management.core.windows.net/", + Stream: "daily", + }, + } + makeTestMetadata(c, env, "precise", "West US", images) + images, err := findMatchingImages(env, "West US", "precise", []string{"amd64"}) c.Assert(err, gc.IsNil) - c.Assert(images, gc.HasLen, 1) - c.Check(images[0].Id, gc.Equals, "MATCHING-IMAGE") + c.Assert(images[0].Id, gc.Equals, "image-id") } func (*instanceTypeSuite) TestNewInstanceTypeConvertsRoleSize(c *gc.C) { @@ -434,22 +301,24 @@ expectation := instances.InstanceType{ Id: roleSize.Name, Name: roleSize.Name, - Arches: []string{"amd64", "i386"}, CpuCores: roleSize.CpuCores, Mem: roleSize.Mem, RootDisk: roleSize.OSDiskSpaceVirt, Cost: roleSize.Cost, - VType: &vtype, + VirtType: &vtype, CpuPower: &cpupower, } - c.Check(newInstanceType(roleSize), gc.DeepEquals, expectation) + c.Assert(newInstanceType(roleSize), gc.DeepEquals, expectation) } -func (*instanceTypeSuite) TestListInstanceTypesAcceptsNil(c *gc.C) { - c.Check(listInstanceTypes(nil), gc.HasLen, 0) +func (s *instanceTypeSuite) TestListInstanceTypesAcceptsNil(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) + types, err := listInstanceTypes(env, nil) + c.Assert(err, gc.IsNil) + c.Check(types, gc.HasLen, 0) } -func (*instanceTypeSuite) TestListInstanceTypesMaintainsOrder(c *gc.C) { +func (s *instanceTypeSuite) TestListInstanceTypesMaintainsOrder(c *gc.C) { roleSizes := []gwacl.RoleSize{ {Name: "Biggish"}, {Name: "Tiny"}, @@ -460,13 +329,17 @@ expectation := make([]instances.InstanceType, len(roleSizes)) for index, roleSize := range roleSizes { expectation[index] = newInstanceType(roleSize) + expectation[index].Arches = []string{"amd64"} } - c.Check(listInstanceTypes(roleSizes), gc.DeepEquals, expectation) + env := s.setupEnvWithDummyMetadata(c) + types, err := listInstanceTypes(env, roleSizes) + c.Assert(err, gc.IsNil) + c.Assert(types, gc.DeepEquals, expectation) } func (*instanceTypeSuite) TestFindInstanceSpecFailsImpossibleRequest(c *gc.C) { - impossibleConstraint := instances.InstanceConstraint{ + impossibleConstraint := &instances.InstanceConstraint{ Series: "precise", Arches: []string{"axp"}, } @@ -477,36 +350,22 @@ c.Check(err, gc.ErrorMatches, "no OS images found for .*") } -// patchFetchImageMetadata temporarily replaces imagemetadata.Fetch() with a -// fake that returns the given canned answer. -// It returns a cleanup function, which you must call when done. -func patchFetchImageMetadata(cannedResponse []*imagemetadata.ImageMetadata, cannedError error) func() { - original := fetchImageMetadata - fetchImageMetadata = func([]simplestreams.DataSource, string, *imagemetadata.ImageConstraint, bool) ([]*imagemetadata.ImageMetadata, error) { - return cannedResponse, cannedError +func makeTestMetadata(c *gc.C, env environs.Environ, series, location string, im []*imagemetadata.ImageMetadata) { + cloudSpec := simplestreams.CloudSpec{ + Region: location, + Endpoint: "https://management.core.windows.net/", } - return func() { fetchImageMetadata = original } + err := imagemetadata.MergeAndWriteMetadata(series, im, &cloudSpec, env.Storage()) + c.Assert(err, gc.IsNil) } -func (*instanceTypeSuite) TestFindInstanceSpecFindsMatch(c *gc.C) { - // We have one OS image. - images := []*imagemetadata.ImageMetadata{ - { - Id: "image-id", - VType: "Hyper-V", - Arch: "amd64", - RegionAlias: "West US", - RegionName: "West US", - Endpoint: "http://localhost/", - }, - } - cleanup := patchFetchImageMetadata(images, nil) - defer cleanup() +func (s *instanceTypeSuite) TestFindInstanceSpecFindsMatch(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) // We'll tailor our constraints to describe one particular Azure // instance type: aim := gwacl.RoleNameMap["Large"] - constraints := instances.InstanceConstraint{ + constraints := &instances.InstanceConstraint{ Region: "West US", Series: "precise", Arches: []string{"amd64"}, @@ -517,7 +376,6 @@ } // Find a matching instance type and image. - env := makeEnviron(c) spec, err := findInstanceSpec(env, constraints) c.Assert(err, gc.IsNil) @@ -527,30 +385,18 @@ c.Check(spec.Image.Id, gc.Equals, "image-id") } -func (*instanceTypeSuite) TestFindInstanceSpecSetsBaseline(c *gc.C) { - images := []*imagemetadata.ImageMetadata{ - { - Id: "image-id", - VType: "Hyper-V", - Arch: "amd64", - RegionAlias: "West US", - RegionName: "West US", - Endpoint: "http://localhost/", - }, - } - cleanup := patchFetchImageMetadata(images, nil) - defer cleanup() +func (s *instanceTypeSuite) TestFindInstanceSpecSetsBaseline(c *gc.C) { + env := s.setupEnvWithDummyMetadata(c) // findInstanceSpec sets baseline constraints, so that it won't pick // ExtraSmall (which is too small for routine tasks) if you fail to // set sufficient hardware constraints. - anyInstanceType := instances.InstanceConstraint{ + anyInstanceType := &instances.InstanceConstraint{ Region: "West US", Series: "precise", Arches: []string{"amd64"}, } - env := makeEnviron(c) spec, err := findInstanceSpec(env, anyInstanceType) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/storage_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/storage_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/azure/storage_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/storage_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,12 +11,12 @@ "net/url" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/gwacl" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/errors" - jc "launchpad.net/juju-core/testing/checkers" ) type storageSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/bootstrap.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/bootstrap.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,7 +11,7 @@ "strings" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" coreCloudinit "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/cloudinit/sshinit" @@ -24,6 +24,7 @@ coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/parallel" + "launchpad.net/juju-core/utils/shell" "launchpad.net/juju-core/utils/ssh" ) @@ -40,6 +41,12 @@ var inst instance.Instance defer func() { handleBootstrapError(err, ctx, inst, env) }() + // First thing, ensure we have tools otherwise there's no point. + selectedTools, err := EnsureBootstrapTools(env, env.Config().DefaultSeries(), cons.Arch) + if err != nil { + return err + } + // Get the bootstrap SSH client. Do this early, so we know // not to bother with any of the below if we can't finish the job. client := ssh.DefaultClient @@ -63,13 +70,12 @@ } machineConfig := environs.NewBootstrapMachineConfig(stateFileURL, privateKey) - selectedTools, err := EnsureBootstrapTools(env, env.Config().DefaultSeries(), cons.Arch) - if err != nil { - return err - } - fmt.Fprintln(ctx.GetStderr(), "Launching instance") - inst, hw, err := env.StartInstance(cons, selectedTools, machineConfig) + inst, hw, err := env.StartInstance(environs.StartInstanceParams{ + Constraints: cons, + Tools: selectedTools, + MachineConfig: machineConfig, + }) if err != nil { return fmt.Errorf("cannot start bootstrap instance: %v", err) } @@ -196,7 +202,12 @@ if err := cloudinit.ConfigureJuju(machineConfig, cloudcfg); err != nil { return err } - return sshinit.Configure(sshinit.ConfigureParams{ + configScript, err := sshinit.ConfigureScript(cloudcfg) + if err != nil { + return err + } + script := shell.DumpFileOnErrorScript(machineConfig.CloudInitOutputLog) + configScript + return sshinit.RunConfigureScript(script, sshinit.ConfigureParams{ Host: "ubuntu@" + addr, Client: client, Config: cloudcfg, @@ -405,18 +416,3 @@ } return bootstrap.SetBootstrapTools(env, possibleTools) } - -// EnsureNotBootstrapped returns null if the environment is not bootstrapped, -// and an error if it is or if the function was not able to tell. -func EnsureNotBootstrapped(env environs.Environ) error { - _, err := bootstrap.LoadState(env.Storage()) - // If there is no error loading the bootstrap state, then we are - // bootstrapped. - if err == nil { - return fmt.Errorf("environment is already bootstrapped") - } - if err == environs.ErrNotBootstrapped { - return nil - } - return err -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/bootstrap_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/bootstrap_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,9 @@ "os" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -21,7 +23,6 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/provider/common" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils/ssh" @@ -35,7 +36,7 @@ var _ = gc.Suite(&BootstrapSuite{}) type cleaner interface { - AddCleanup(testbase.CleanupFunc) + AddCleanup(testing.CleanupFunc) } func (s *BootstrapSuite) SetUpTest(c *gc.C) { @@ -80,7 +81,7 @@ Storage: newStorage(s, c), putErr: fmt.Errorf("noes!"), } - env := &mockEnviron{storage: brokenStorage} + env := &mockEnviron{storage: brokenStorage, config: configGetter(c)} ctx := coretesting.Context(c) err := common.Bootstrap(ctx, env, constraints.Value{}) c.Assert(err, gc.ErrorMatches, "cannot create initial state file: noes!") @@ -93,7 +94,7 @@ checkCons := constraints.MustParse("mem=8G") startInstance := func( - cons constraints.Value, possibleTools tools.List, mcfg *cloudinit.MachineConfig, + cons constraints.Value, _ environs.Networks, possibleTools tools.List, mcfg *cloudinit.MachineConfig, ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { @@ -118,7 +119,7 @@ stor := &mockStorage{Storage: innerStorage} startInstance := func( - _ constraints.Value, _ tools.List, _ *cloudinit.MachineConfig, + _ constraints.Value, _ environs.Networks, _ tools.List, _ *cloudinit.MachineConfig, ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { @@ -151,7 +152,7 @@ stor := &mockStorage{Storage: innerStorage} startInstance := func( - _ constraints.Value, _ tools.List, _ *cloudinit.MachineConfig, + _ constraints.Value, _ environs.Networks, _ tools.List, _ *cloudinit.MachineConfig, ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { @@ -193,7 +194,7 @@ checkURL := "" startInstance := func( - _ constraints.Value, _ tools.List, mcfg *cloudinit.MachineConfig, + _ constraints.Value, _ environs.Networks, _ tools.List, mcfg *cloudinit.MachineConfig, ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/destroy_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/destroy_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/destroy_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/destroy_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,13 +7,13 @@ "fmt" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/provider/common" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/mock_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/mock_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/mock_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/mock_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -17,7 +17,7 @@ ) type allInstancesFunc func() ([]instance.Instance, error) -type startInstanceFunc func(constraints.Value, tools.List, *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) +type startInstanceFunc func(constraints.Value, environs.Networks, tools.List, *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) type stopInstancesFunc func([]instance.Instance) error type getToolsSourcesFunc func() ([]simplestreams.DataSource, error) type configFunc func() *config.Config @@ -38,6 +38,10 @@ return "mock environment" } +func (*mockEnviron) SupportedArchitectures() ([]string, error) { + return []string{"amd64", "arm64"}, nil +} + func (env *mockEnviron) Storage() storage.Storage { return env.storage } @@ -45,12 +49,8 @@ func (env *mockEnviron) AllInstances() ([]instance.Instance, error) { return env.allInstances() } -func (env *mockEnviron) StartInstance( - cons constraints.Value, possibleTools tools.List, mcfg *cloudinit.MachineConfig, -) ( - instance.Instance, *instance.HardwareCharacteristics, error, -) { - return env.startInstance(cons, possibleTools, mcfg) +func (env *mockEnviron) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { + return env.startInstance(args.Constraints, args.Networks, args.Tools, args.MachineConfig) } func (env *mockEnviron) StopInstances(instances []instance.Instance) error { @@ -72,7 +72,12 @@ if env.getToolsSources != nil { return env.getToolsSources() } - datasource := storage.NewStorageSimpleStreamsDataSource(env.Storage(), storage.BaseToolsPath) + datasource := storage.NewStorageSimpleStreamsDataSource("test cloud storage", env.Storage(), storage.BaseToolsPath) + return []simplestreams.DataSource{datasource}, nil +} + +func (env *mockEnviron) GetImageSources() ([]simplestreams.DataSource, error) { + datasource := storage.NewStorageSimpleStreamsDataSource("test cloud storage", env.Storage(), storage.BaseImagesPath) return []simplestreams.DataSource{datasource}, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/supportedarchitectures.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/supportedarchitectures.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/supportedarchitectures.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/supportedarchitectures.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,28 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/imagemetadata" + "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/utils/set" +) + +// SupportedArchitectures returns all the image architectures for env matching the constraints. +func SupportedArchitectures(env environs.Environ, imageConstraint *imagemetadata.ImageConstraint) ([]string, error) { + sources, err := imagemetadata.GetMetadataSources(env) + if err != nil { + return nil, err + } + matchingImages, _, err := imagemetadata.Fetch(sources, simplestreams.DefaultIndexPath, imageConstraint, false) + if err != nil { + return nil, err + } + var arches = set.NewStrings() + for _, im := range matchingImages { + arches.Add(im.Arch) + } + return arches.Values(), nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/supportedarchitectures_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/supportedarchitectures_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/common/supportedarchitectures_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/supportedarchitectures_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,83 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common_test + +import ( + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/imagemetadata" + "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/provider/common" + "launchpad.net/juju-core/testing/testbase" +) + +type archSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&archSuite{}) + +func (s *archSuite) setupMetadata(c *gc.C, arches []string) (environs.Environ, simplestreams.CloudSpec) { + s.PatchValue(&imagemetadata.DefaultBaseURL, "") + stor := newStorage(s, c) + env := &mockEnviron{ + storage: stor, + config: configGetter(c), + } + + var images []*imagemetadata.ImageMetadata + for _, arch := range arches { + images = append(images, &imagemetadata.ImageMetadata{ + Id: "image-id", + Arch: arch, + RegionName: "Region", + Endpoint: "https://endpoint/", + }) + } + // Append an image from another region with some other arch to ensure it is ignored. + images = append(images, &imagemetadata.ImageMetadata{ + Id: "image-id", + Arch: "arch", + RegionName: "Region-Two", + Endpoint: "https://endpoint/", + }) + cloudSpec := simplestreams.CloudSpec{ + Region: "Region", + Endpoint: "https://endpoint/", + } + err := imagemetadata.MergeAndWriteMetadata("precise", images, &cloudSpec, env.Storage()) + c.Assert(err, gc.IsNil) + return env, cloudSpec +} + +func (s *archSuite) TestSupportedArchitecturesNone(c *gc.C) { + env, cloudSpec := s.setupMetadata(c, nil) + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + }) + arches, err := common.SupportedArchitectures(env, imageConstraint) + c.Assert(err, gc.IsNil) + c.Assert(arches, gc.HasLen, 0) +} + +func (s *archSuite) TestSupportedArchitecturesOne(c *gc.C) { + env, cloudSpec := s.setupMetadata(c, []string{"ppc64"}) + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + }) + arches, err := common.SupportedArchitectures(env, imageConstraint) + c.Assert(err, gc.IsNil) + c.Assert(arches, gc.DeepEquals, []string{"ppc64"}) +} + +func (s *archSuite) TestSupportedArchitecturesMany(c *gc.C) { + env, cloudSpec := s.setupMetadata(c, []string{"ppc64", "amd64"}) + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + }) + arches, err := common.SupportedArchitectures(env, imageConstraint) + c.Assert(err, gc.IsNil) + c.Assert(arches, gc.DeepEquals, []string{"amd64", "ppc64"}) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/dummy/environs.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/dummy/environs.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/dummy/environs.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/dummy/environs.go 2014-03-20 12:52:38.000000000 +0000 @@ -33,18 +33,18 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/bootstrap" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/names" "launchpad.net/juju-core/provider" "launchpad.net/juju-core/provider/common" @@ -53,7 +53,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/apiserver" "launchpad.net/juju-core/testing" - coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" ) @@ -526,16 +525,21 @@ return e.name } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (*environ) SupportedArchitectures() ([]string, error) { + return []string{arch.AMD64, arch.PPC64}, nil +} + // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseImagesPath)}, nil + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)}, nil } // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil } func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { @@ -677,11 +681,10 @@ } // StartInstance is specified in the InstanceBroker interface. -func (e *environ) StartInstance(cons constraints.Value, possibleTools coretools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (e *environ) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { defer delay() - machineId := machineConfig.MachineId + machineId := args.MachineConfig.MachineId logger.Infof("dummy startinstance, machine %s", machineId) if err := e.checkBroken("StartInstance"); err != nil { return nil, nil, err @@ -692,20 +695,20 @@ } estate.mu.Lock() defer estate.mu.Unlock() - if machineConfig.MachineNonce == "" { + if args.MachineConfig.MachineNonce == "" { return nil, nil, fmt.Errorf("cannot start instance: missing machine nonce") } if _, ok := e.Config().CACert(); !ok { return nil, nil, fmt.Errorf("no CA certificate in environment configuration") } - if machineConfig.StateInfo.Tag != names.MachineTag(machineId) { + if args.MachineConfig.StateInfo.Tag != names.MachineTag(machineId) { return nil, nil, fmt.Errorf("entity tag must match started machine") } - if machineConfig.APIInfo.Tag != names.MachineTag(machineId) { + if args.MachineConfig.APIInfo.Tag != names.MachineTag(machineId) { return nil, nil, fmt.Errorf("entity tag must match started machine") } - logger.Infof("would pick tools from %s", possibleTools) - series := possibleTools.OneSeries() + logger.Infof("would pick tools from %s", args.Tools) + series := args.Tools.OneSeries() i := &dummyInstance{ id: instance.Id(fmt.Sprintf("%s-%d", e.name, estate.maxId)), ports: make(map[instance.Port]bool), @@ -721,12 +724,12 @@ // We will just assume the instance hardware characteristics exactly matches // the supplied constraints (if specified). hc = &instance.HardwareCharacteristics{ - Arch: cons.Arch, - Mem: cons.Mem, - RootDisk: cons.RootDisk, - CpuCores: cons.CpuCores, - CpuPower: cons.CpuPower, - Tags: cons.Tags, + Arch: args.Constraints.Arch, + Mem: args.Constraints.Mem, + RootDisk: args.Constraints.RootDisk, + CpuCores: args.Constraints.CpuCores, + CpuPower: args.Constraints.CpuPower, + Tags: args.Constraints.Tags, } // Fill in some expected instance hardware characteristics if constraints not specified. if hc.Arch == nil { @@ -751,11 +754,11 @@ estate.ops <- OpStartInstance{ Env: e.name, MachineId: machineId, - MachineNonce: machineConfig.MachineNonce, - Constraints: cons, + MachineNonce: args.MachineConfig.MachineNonce, + Constraints: args.Constraints, Instance: i, - Info: machineConfig.StateInfo, - APIInfo: machineConfig.APIInfo, + Info: args.MachineConfig.StateInfo, + APIInfo: args.MachineConfig.APIInfo, Secret: e.ecfg().secret(), } return i, hc, nil diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/ec2.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/ec2.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/ec2.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/ec2.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,14 +11,13 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/goamz/aws" "launchpad.net/goamz/ec2" "launchpad.net/goamz/s3" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/instances" @@ -26,6 +25,7 @@ "launchpad.net/juju-core/environs/storage" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" @@ -52,6 +52,12 @@ type environ struct { name string + // archMutex gates access to supportedArchitectures + archMutex sync.Mutex + // supportedArchitectures caches the architectures + // for which images can be instantiated. + supportedArchitectures []string + // ecfgMutex protects the *Unlocked fields below. ecfgMutex sync.Mutex ecfgUnlocked *environConfig @@ -179,18 +185,25 @@ # https://juju.ubuntu.com/docs/config-aws.html amazon: type: ec2 - # region specifies the ec2 region. It defaults to us-east-1. + + # region specifies the EC2 region. It defaults to us-east-1. + # # region: us-east-1 + + # access-key holds the EC2 access key. It defaults to the + # environment variable AWS_ACCESS_KEY_ID. # - # access-key holds the ec2 access key. It defaults to the environment - # variable AWS_ACCESS_KEY_ID. # access-key: + + # secret-key holds the EC2 secret key. It defaults to the + # environment variable AWS_SECRET_ACCESS_KEY. # - # secret-key holds the ec2 secret key. It defaults to the environment - # variable AWS_SECRET_ACCESS_KEY. + # secret-key: + + # image-stream chooses a simplestreams stream to select OS images + # from, for example daily or released images (or any other stream + # available on simplestreams). # - # image-stream chooses a simplestreams stream to select OS images from, - # for example daily or released images (or any other stream available on simplestreams). # image-stream: "released" `[1:] @@ -236,7 +249,7 @@ return &simplestreams.MetadataLookupParams{ Region: region, Endpoint: ec2Region.EC2Endpoint, - Architectures: []string{"amd64", "i386", "arm"}, + Architectures: arch.AllSupportedArches, }, nil } @@ -325,26 +338,49 @@ return common.StateInfo(e) } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (e *environ) SupportedArchitectures() ([]string, error) { + e.archMutex.Lock() + defer e.archMutex.Unlock() + if e.supportedArchitectures != nil { + return e.supportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + cloudSpec, err := e.Region() + if err != nil { + return nil, err + } + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + Stream: e.Config().ImageStream(), + }) + e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) + return e.supportedArchitectures, err +} + // MetadataLookupParams returns parameters which are used to query simplestreams metadata. func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { if region == "" { region = e.ecfg().region() } - ec2Region, ok := allRegions[region] - if !ok { - return nil, fmt.Errorf("unknown region %q", region) + cloudSpec, err := e.cloudSpec(region) + if err != nil { + return nil, err } return &simplestreams.MetadataLookupParams{ Series: e.ecfg().DefaultSeries(), - Region: region, - Endpoint: ec2Region.EC2Endpoint, - Architectures: []string{"amd64", "i386", "arm"}, + Region: cloudSpec.Region, + Endpoint: cloudSpec.Endpoint, + Architectures: arch.AllSupportedArches, }, nil } // Region is specified in the HasRegion interface. func (e *environ) Region() (simplestreams.CloudSpec, error) { - region := e.ecfg().region() + return e.cloudSpec(e.ecfg().region()) +} + +func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { ec2Region, ok := allRegions[region] if !ok { return simplestreams.CloudSpec{}, fmt.Errorf("unknown region %q", region) @@ -358,50 +394,49 @@ const ebsStorage = "ebs" // StartInstance is specified in the InstanceBroker interface. -func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (e *environ) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { - arches := possibleTools.Arches() + arches := args.Tools.Arches() stor := ebsStorage sources, err := imagemetadata.GetMetadataSources(e) if err != nil { return nil, nil, err } - series := possibleTools.OneSeries() + series := args.Tools.OneSeries() spec, err := findInstanceSpec(sources, e.Config().ImageStream(), &instances.InstanceConstraint{ Region: e.ecfg().region(), Series: series, Arches: arches, - Constraints: cons, + Constraints: args.Constraints, Storage: &stor, }) if err != nil { return nil, nil, err } - tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch}) + tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) if err != nil { return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) } - machineConfig.Tools = tools[0] - if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil { + args.MachineConfig.Tools = tools[0] + if err := environs.FinishMachineConfig(args.MachineConfig, e.Config(), args.Constraints); err != nil { return nil, nil, err } - userData, err := environs.ComposeUserData(machineConfig) + userData, err := environs.ComposeUserData(args.MachineConfig) if err != nil { return nil, nil, fmt.Errorf("cannot make user data: %v", err) } logger.Debugf("ec2 user data; %d bytes", len(userData)) cfg := e.Config() - groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) + groups, err := e.setUpGroups(args.MachineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) if err != nil { return nil, nil, fmt.Errorf("cannot set up groups: %v", err) } var instResp *ec2.RunInstancesResp - device, diskSize := getDiskSize(cons) + device, diskSize := getDiskSize(args.Constraints) for a := shortAttempt.Start(); a.Next(); { instResp, err = e.ec2().RunInstances(&ec2.RunInstances{ ImageId: spec.Image.Id, @@ -1057,7 +1092,7 @@ func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. sources := []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseImagesPath)} + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)} return sources, nil } @@ -1065,6 +1100,6 @@ func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. sources := []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)} + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)} return sources, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/image.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/image.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image.go 2014-03-20 12:52:38.000000000 +0000 @@ -48,7 +48,7 @@ Arches: ic.Arches, Stream: stream, }) - matchingImages, err := imagemetadata.Fetch( + matchingImages, _, err := imagemetadata.Fetch( sources, simplestreams.DefaultIndexPath, imageConstraint, signedImageDataOnly) if err != nil { return nil, err diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/image_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/image_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -128,7 +128,8 @@ c.Logf("test %d", i) stor := ebsStorage spec, err := findInstanceSpec( - []simplestreams.DataSource{simplestreams.NewURLDataSource("test:", simplestreams.VerifySSLHostnames)}, + []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames)}, "released", &instances.InstanceConstraint{ Region: "test", @@ -169,7 +170,8 @@ for i, t := range findInstanceSpecErrorTests { c.Logf("test %d", i) _, err := findInstanceSpec( - []simplestreams.DataSource{simplestreams.NewURLDataSource("test:", simplestreams.VerifySSLHostnames)}, + []simplestreams.DataSource{ + simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames)}, "released", &instances.InstanceConstraint{ Region: "test", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/instancetype.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/instancetype.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/instancetype.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/instancetype.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "launchpad.net/goamz/aws" "launchpad.net/juju-core/environs/instances" + "launchpad.net/juju-core/juju/arch" ) // Type of virtualisation used. @@ -17,8 +18,8 @@ // all instance types can run amd64 images, and some can also run i386 ones. var ( - amd64 = []string{"amd64"} - both = []string{"amd64", "i386"} + amd64 = []string{arch.AMD64} + both = []string{arch.AMD64, arch.I386} ) // allRegions is defined here to allow tests to override the content. @@ -35,28 +36,28 @@ CpuCores: 1, CpuPower: instances.CpuPower(100), Mem: 1740, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m1.medium", Arches: both, CpuCores: 1, CpuPower: instances.CpuPower(200), Mem: 3840, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m1.large", Arches: amd64, CpuCores: 2, CpuPower: instances.CpuPower(400), Mem: 7680, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m1.xlarge", Arches: amd64, CpuCores: 4, CpuPower: instances.CpuPower(800), Mem: 15360, - VType: ¶virtual, + VirtType: ¶virtual, }, { // Second generation. Name: "m3.medium", @@ -64,28 +65,28 @@ CpuCores: 1, CpuPower: instances.CpuPower(300), Mem: 3840, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m3.large", Arches: amd64, CpuCores: 2, CpuPower: instances.CpuPower(6500), Mem: 7680, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m3.xlarge", Arches: amd64, CpuCores: 4, CpuPower: instances.CpuPower(1300), Mem: 15360, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m3.2xlarge", Arches: amd64, CpuCores: 8, CpuPower: instances.CpuPower(2600), Mem: 30720, - VType: ¶virtual, + VirtType: ¶virtual, }, { // Micro. Name: "t1.micro", @@ -93,7 +94,7 @@ CpuCores: 1, CpuPower: instances.CpuPower(20), Mem: 613, - VType: ¶virtual, + VirtType: ¶virtual, }, { // High-Memory. Name: "m2.xlarge", @@ -101,21 +102,21 @@ CpuCores: 2, CpuPower: instances.CpuPower(650), Mem: 17408, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m2.2xlarge", Arches: amd64, CpuCores: 4, CpuPower: instances.CpuPower(1300), Mem: 34816, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "m2.4xlarge", Arches: amd64, CpuCores: 8, CpuPower: instances.CpuPower(2600), Mem: 69632, - VType: ¶virtual, + VirtType: ¶virtual, }, { // High-CPU. Name: "c1.medium", @@ -123,14 +124,14 @@ CpuCores: 2, CpuPower: instances.CpuPower(500), Mem: 1740, - VType: ¶virtual, + VirtType: ¶virtual, }, { Name: "c1.xlarge", Arches: amd64, CpuCores: 8, CpuPower: instances.CpuPower(2000), Mem: 7168, - VType: ¶virtual, + VirtType: ¶virtual, }, { // Cluster compute. Name: "cc1.4xlarge", @@ -138,14 +139,14 @@ CpuCores: 8, CpuPower: instances.CpuPower(3350), Mem: 23552, - VType: &hvm, + VirtType: &hvm, }, { Name: "cc2.8xlarge", Arches: amd64, CpuCores: 16, CpuPower: instances.CpuPower(8800), Mem: 61952, - VType: &hvm, + VirtType: &hvm, }, { // High Memory cluster. Name: "cr1.8xlarge", @@ -153,7 +154,7 @@ CpuCores: 16, CpuPower: instances.CpuPower(8800), Mem: 249856, - VType: &hvm, + VirtType: &hvm, }, { // Cluster GPU. Name: "cg1.4xlarge", @@ -161,7 +162,7 @@ CpuCores: 8, CpuPower: instances.CpuPower(3350), Mem: 22528, - VType: &hvm, + VirtType: &hvm, }, { // High I/O. Name: "hi1.4xlarge", @@ -169,7 +170,7 @@ CpuCores: 16, CpuPower: instances.CpuPower(3500), Mem: 61952, - VType: ¶virtual, + VirtType: ¶virtual, }, { // High storage. Name: "hs1.8xlarge", @@ -177,7 +178,7 @@ CpuCores: 16, CpuPower: instances.CpuPower(3500), Mem: 119808, - VType: ¶virtual, + VirtType: ¶virtual, }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/live_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/live_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/live_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/live_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "io" "strings" + jc "github.com/juju/testing/checkers" amzec2 "launchpad.net/goamz/ec2" gc "launchpad.net/gocheck" @@ -19,10 +20,10 @@ "launchpad.net/juju-core/environs/storage" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/ec2" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -98,6 +99,12 @@ func (t *LiveTests) SetUpTest(c *gc.C) { t.LoggingSuite.SetUpTest(c) t.LiveTests.SetUpTest(c) + t.PatchValue(&version.Current, version.Binary{ + Number: version.Current.Number, + Series: config.DefaultSeries, + Arch: arch.AMD64, + }) + } func (t *LiveTests) TearDownTest(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/local_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/local_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/ec2/local_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/local_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "sort" "strings" + jc "github.com/juju/testing/checkers" "launchpad.net/goamz/aws" amzec2 "launchpad.net/goamz/ec2" "launchpad.net/goamz/ec2/ec2test" @@ -28,10 +29,10 @@ envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/ec2" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/ssh" @@ -222,6 +223,11 @@ func (t *localServerSuite) SetUpTest(c *gc.C) { t.srv.startServer(c) t.Tests.SetUpTest(c) + t.PatchValue(&version.Current, version.Binary{ + Number: version.Current.Number, + Series: config.DefaultSeries, + Arch: arch.AMD64, + }) } func (t *localServerSuite) TearDownTest(c *gc.C) { @@ -383,7 +389,7 @@ params.Endpoint = "https://ec2.endpoint.com" params.Sources, err = imagemetadata.GetMetadataSources(env) c.Assert(err, gc.IsNil) - image_ids, err := imagemetadata.ValidateImageMetadata(params) + image_ids, _, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.IsNil) sort.Strings(image_ids) c.Assert(image_ids, gc.DeepEquals, []string{"ami-00000033", "ami-00000034", "ami-00000035"}) @@ -399,6 +405,13 @@ c.Assert(strings.Contains(url, ec2.ControlBucketName(env)+"/tools"), jc.IsTrue) } +func (t *localServerSuite) TestSupportedArchitectures(c *gc.C) { + env := t.Prepare(c) + a, err := env.SupportedArchitectures() + c.Assert(err, gc.IsNil) + c.Assert(a, gc.DeepEquals, []string{"amd64", "i386"}) +} + // localNonUSEastSuite is similar to localServerSuite but the S3 mock server // behaves as if it is not in the us-east region. type localNonUSEastSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/config.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,25 +14,46 @@ // boilerplateConfig will be shown in help output, so please keep it up to // date when you change environment configuration below. -const boilerplateConfig = `joyent: - type: joyent +var boilerplateConfig = ` +joyent: + type: joyent - # SDC config - # Can be set via env variables, or specified here - # sdc-user: - # Can be set via env variables, or specified here - # sdc-key-id: - # region defaults to us-west-1, override if required - # sdc-region: us-west-1 - - # Manta config - # Can be set via env variables, or specified here - # manta-user: - # Can be set via env variables, or specified here - # manta-key-id: - # region defaults to us-east, override if required - # manta-region: us-east -` + ## SDC config + + # sdc-user holds the SDC user. It can also be specified in the + # SDC_ACCOUNT environment variable. + # + # sdc-user: + + # sdc-key-id holds the fingerprint of one of user's SSH keys. It + # can also be specified in the SDC_KEY_ID environment variable. + # + # sdc-key-id: + + # sdc-region holds the region to use: us-west-1 (default), + # us-east-1, us-sw-1, eu-ams-1. Override if required. + # + # sdc-region: us-west-1 + + ## Manta config + + # manta-user holds the user's Manta account name. It can also be + # specified in the MANTA_USER environment variable. + # + # manta-user: + + # manta-key-id holds the fingerprint of one of the user's SSH + # keys. It can also be specified in the MANTA_KEY_ID environment + # variable. + # + # manta-key-id: + + # manta-region holds the Manta region to use. It defaults to + # us-east, override if required + # + # manta-region: us-east + +`[1:] const ( SdcAccount = "SDC_ACCOUNT" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,24 +4,25 @@ package joyent_test import ( + "github.com/juju/testing" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/provider/joyent" - "launchpad.net/juju-core/testing" + coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" ) -func newConfig(c *gc.C, attrs testing.Attrs) *config.Config { - attrs = testing.FakeConfig().Merge(attrs) +func newConfig(c *gc.C, attrs coretesting.Attrs) *config.Config { + attrs = coretesting.FakeConfig().Merge(attrs) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, gc.IsNil) return cfg } -func validAttrs() testing.Attrs { - return testing.FakeConfig().Merge(testing.Attrs{ +func validAttrs() coretesting.Attrs { + return coretesting.FakeConfig().Merge(coretesting.Attrs{ "type": "joyent", "sdc-user": "dstroppa", "sdc-key-id": "12:c3:a7:cb:a2:29:e2:90:88:3f:04:53:3b:4e:75:40", @@ -35,7 +36,7 @@ type ConfigSuite struct { testbase.LoggingSuite - originalValues map[string]testbase.Restorer + originalValues map[string]testing.Restorer } var _ = gc.Suite(&ConfigSuite{}) @@ -49,9 +50,9 @@ var newConfigTests = []struct { info string - insert testing.Attrs + insert coretesting.Attrs remove []string - expect testing.Attrs + expect coretesting.Attrs err string }{{ info: "sdc-user is required", @@ -59,7 +60,7 @@ err: "sdc-user: expected string, got nothing", }, { info: "sdc-user cannot be empty", - insert: testing.Attrs{"sdc-user": ""}, + insert: coretesting.Attrs{"sdc-user": ""}, err: "sdc-user: must not be empty", }, { info: "sdc-key-id is required", @@ -67,26 +68,26 @@ err: "sdc-key-id: expected string, got nothing", }, { info: "sdc-key-id cannot be empty", - insert: testing.Attrs{"sdc-key-id": ""}, + insert: coretesting.Attrs{"sdc-key-id": ""}, err: "sdc-key-id: must not be empty", }, { info: "sdc-region is inserted if missing", - expect: testing.Attrs{"sdc-region": "us-west-1"}, + expect: coretesting.Attrs{"sdc-region": "us-west-1"}, }, { info: "sdc-region cannot be empty", - insert: testing.Attrs{"sdc-region": ""}, + insert: coretesting.Attrs{"sdc-region": ""}, err: "sdc-region: must not be empty", }, { info: "sdc-region is untouched if present", - insert: testing.Attrs{"sdc-region": "us-west-1"}, - expect: testing.Attrs{"sdc-region": "us-west-1"}, + insert: coretesting.Attrs{"sdc-region": "us-west-1"}, + expect: coretesting.Attrs{"sdc-region": "us-west-1"}, }, { info: "manta-user is required", remove: []string{"manta-user"}, err: "manta-user: expected string, got nothing", }, { info: "manta-user cannot be empty", - insert: testing.Attrs{"manta-user": ""}, + insert: coretesting.Attrs{"manta-user": ""}, err: "manta-user: must not be empty", }, { info: "manta-key-id is required", @@ -94,23 +95,23 @@ err: "manta-key-id: expected string, got nothing", }, { info: "manta-key-id cannot be empty", - insert: testing.Attrs{"manta-key-id": ""}, + insert: coretesting.Attrs{"manta-key-id": ""}, err: "manta-key-id: must not be empty", }, { info: "manta-region is inserted if missing", - expect: testing.Attrs{"manta-region": "us-east"}, + expect: coretesting.Attrs{"manta-region": "us-east"}, }, { info: "manta-region cannot be empty", - insert: testing.Attrs{"manta-region": ""}, + insert: coretesting.Attrs{"manta-region": ""}, err: "manta-region: must not be empty", }, { info: "manta-region is untouched if present", - insert: testing.Attrs{"manta-region": "us-east"}, - expect: testing.Attrs{"manta-region": "us-east"}, + insert: coretesting.Attrs{"manta-region": "us-east"}, + expect: coretesting.Attrs{"manta-region": "us-east"}, }, { info: "unknown field is not touched", - insert: testing.Attrs{"unknown-field": 12345}, - expect: testing.Attrs{"unknown-field": 12345}, + insert: coretesting.Attrs{"unknown-field": 12345}, + expect: coretesting.Attrs{"unknown-field": 12345}, }} func (*ConfigSuite) TestNewEnvironConfig(c *gc.C) { @@ -173,41 +174,41 @@ var changeConfigTests = []struct { info string - insert testing.Attrs + insert coretesting.Attrs remove []string - expect testing.Attrs + expect coretesting.Attrs err string }{{ info: "no change, no error", expect: validAttrs(), }, { info: "can change sdc-user", - insert: testing.Attrs{"sdc-user": "joyent_user"}, - expect: testing.Attrs{"sdc-user": "joyent_user"}, + insert: coretesting.Attrs{"sdc-user": "joyent_user"}, + expect: coretesting.Attrs{"sdc-user": "joyent_user"}, }, { info: "can change sdc-key-id", - insert: testing.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, - expect: testing.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + insert: coretesting.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + expect: coretesting.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, }, { info: "can change sdc-region", - insert: testing.Attrs{"sdc-region": "us-west-1"}, - expect: testing.Attrs{"sdc-region": "us-west-1"}, + insert: coretesting.Attrs{"sdc-region": "us-west-1"}, + expect: coretesting.Attrs{"sdc-region": "us-west-1"}, }, { info: "can change manta-user", - insert: testing.Attrs{"manta-user": "manta_user"}, - expect: testing.Attrs{"manta-user": "manta_user"}, + insert: coretesting.Attrs{"manta-user": "manta_user"}, + expect: coretesting.Attrs{"manta-user": "manta_user"}, }, { info: "can change manta-key-id", - insert: testing.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, - expect: testing.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + insert: coretesting.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + expect: coretesting.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, }, { info: "can change manta-region", - insert: testing.Attrs{"manta-region": "us-east"}, - expect: testing.Attrs{"manta-region": "us-east"}, + insert: coretesting.Attrs{"manta-region": "us-east"}, + expect: coretesting.Attrs{"manta-region": "us-east"}, }, { info: "can insert unknown field", - insert: testing.Attrs{"unknown": "ignoti"}, - expect: testing.Attrs{"unknown": "ignoti"}, + insert: coretesting.Attrs{"unknown": "ignoti"}, + expect: coretesting.Attrs{"unknown": "ignoti"}, }} func (s *ConfigSuite) TestValidateChange(c *gc.C) { @@ -256,29 +257,29 @@ var prepareConfigTests = []struct { info string - insert testing.Attrs + insert coretesting.Attrs remove []string - expect testing.Attrs + expect coretesting.Attrs err string }{{ info: "All value provided, nothig to do", expect: validAttrs(), }, { info: "can get sdc-user from env variable", - insert: testing.Attrs{"sdc-user": ""}, - expect: testing.Attrs{"sdc-user": "tester"}, + insert: coretesting.Attrs{"sdc-user": ""}, + expect: coretesting.Attrs{"sdc-user": "tester"}, }, { info: "can get sdc-key-id from env variable", - insert: testing.Attrs{"sdc-key-id": ""}, - expect: testing.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + insert: coretesting.Attrs{"sdc-key-id": ""}, + expect: coretesting.Attrs{"sdc-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, }, { info: "can get manta-user from env variable", - insert: testing.Attrs{"manta-user": ""}, - expect: testing.Attrs{"manta-user": "tester"}, + insert: coretesting.Attrs{"manta-user": ""}, + expect: coretesting.Attrs{"manta-user": "tester"}, }, { info: "can get manta-key-id from env variable", - insert: testing.Attrs{"manta-key-id": ""}, - expect: testing.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, + insert: coretesting.Attrs{"manta-key-id": ""}, + expect: coretesting.Attrs{"manta-key-id": "11:c4:b6:c0:a3:24:22:96:a8:1f:07:53:3f:8e:14:7a"}, }} func (s *ConfigSuite) TestPrepare(c *gc.C) { @@ -286,7 +287,7 @@ c.Logf("test %d: %s", i, test.info) attrs := validAttrs().Merge(test.insert).Delete(test.remove...) testConfig := newConfig(c, attrs) - preparedConfig, err := joyent.Provider.Prepare(testing.Context(c), testConfig) + preparedConfig, err := joyent.Provider.Prepare(coretesting.Context(c), testConfig) if test.err == "" { c.Assert(err, gc.IsNil) attrs := preparedConfig.Config().AllAttrs() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,8 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/environs/imagemetadata" + "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" @@ -25,6 +27,11 @@ type environ struct { name string + + // supportedArchitectures caches the architectures + // for which images can be instantiated. + supportedArchitectures []string + // All mutating operations should lock the mutex. Non-mutating operations // should read all fields (other than name, which is immutable) from a // shallow copy taken with getSnapshot(). @@ -45,6 +52,28 @@ return providerInstance } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (e *environ) SupportedArchitectures() ([]string, error) { + e.lock.Lock() + defer e.lock.Unlock() + if e.supportedArchitectures != nil { + return e.supportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + cloudSpec := simplestreams.CloudSpec{ + // TODO - add in region and url when known. + // Region: e.Config().sdcRegion(), + // Endpoint: "", + } + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + Stream: e.Config().ImageStream(), + }) + var err error + e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) + return e.supportedArchitectures, err +} + func (env *environ) SetConfig(cfg *config.Config) error { env.lock.Lock() defer env.lock.Unlock() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/environ_instance.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/environ_instance.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/environ_instance.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/environ_instance.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,15 +4,11 @@ package joyent import ( - "launchpad.net/juju-core/constraints" - "launchpad.net/juju-core/environs/cloudinit" + "launchpad.net/juju-core/environs" "launchpad.net/juju-core/instance" - "launchpad.net/juju-core/tools" ) -func (env *environ) StartInstance( - cons constraints.Value, possibleTools tools.List, machineConf *cloudinit.MachineConfig, -) ( +func (env *environ) StartInstance(args environs.StartInstanceParams) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { // Please note that in order to fulfil the demands made of Instances and diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/provider.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/provider.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/joyent/provider.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/joyent/provider.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "errors" "fmt" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/config.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -25,6 +25,8 @@ "container": schema.String(), "storage-port": schema.ForceInt(), "namespace": schema.String(), + "lxc-clone": schema.Bool(), + "lxc-clone-aufs": schema.Bool(), } // The port defaults below are not entirely arbitrary. Local user web // frameworks often use 8000 or 8080, so I didn't want to use either of @@ -37,6 +39,8 @@ "bootstrap-ip": schema.Omit, "storage-port": 8040, "namespace": "", + "lxc-clone": schema.Omit, + "lxc-clone-aufs": schema.Omit, } ) @@ -103,6 +107,16 @@ return filepath.Join(c.rootDir(), filename) } +func (c *environConfig) lxcClone() bool { + value, _ := c.attrs["lxc-clone"].(bool) + return value +} + +func (c *environConfig) lxcCloneAUFS() bool { + value, _ := c.attrs["lxc-clone-aufs"].(bool) + return value +} + func (c *environConfig) createDirs() error { for _, dirname := range []string{ c.storageDir(), diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -1,4 +1,4 @@ -// Copyright 2013 Canonical Ltd. +// Copyright 2013, 2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package local @@ -10,8 +10,12 @@ "os/exec" "path/filepath" "regexp" + "strconv" "strings" "sync" + "syscall" + + "github.com/errgo/errgo" "launchpad.net/juju-core/agent" coreCloudinit "launchpad.net/juju-core/cloudinit" @@ -29,11 +33,13 @@ "launchpad.net/juju-core/environs/storage" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" - "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/utils/shell" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/terminationworker" ) @@ -62,7 +68,13 @@ func (e *localEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil +} + +// SupportedArchitectures is specified on the EnvironCapability interface. +func (*localEnviron) SupportedArchitectures() ([]string, error) { + localArch := arch.HostArch() + return []string{localArch}, nil } // Name is specified in the Environ interface. @@ -126,24 +138,19 @@ return err } - bootstrapJobs, err := agent.MarshalBootstrapJobs(state.JobManageEnviron) - if err != nil { - return err - } - mcfg := environs.NewBootstrapMachineConfig(stateFileURL, privateKey) mcfg.Tools = selectedTools[0] mcfg.DataDir = env.config.rootDir() mcfg.LogDir = fmt.Sprintf("/var/log/juju-%s", env.config.namespace()) + mcfg.Jobs = []params.MachineJob{params.JobManageEnviron} mcfg.CloudInitOutputLog = filepath.Join(env.config.logDir(), "cloud-init-output.log") mcfg.DisablePackageCommands = true mcfg.MachineAgentServiceName = env.machineAgentServiceName() mcfg.MongoServiceName = env.mongoServiceName() mcfg.AgentEnvironment = map[string]string{ - agent.Namespace: env.config.namespace(), - agent.StorageDir: env.config.storageDir(), - agent.StorageAddr: env.config.storageAddr(), - agent.BootstrapJobs: bootstrapJobs, + agent.Namespace: env.config.namespace(), + agent.StorageDir: env.config.storageDir(), + agent.StorageAddr: env.config.storageAddr(), } if err := environs.FinishMachineConfig(mcfg, cfg, cons); err != nil { return err @@ -162,6 +169,7 @@ cloudcfg.AddScripts( fmt.Sprintf("rm -fr %s", mcfg.LogDir), fmt.Sprintf("mkdir -p %s", mcfg.LogDir), + fmt.Sprintf("chown syslog:adm %s", mcfg.LogDir), fmt.Sprintf("rm -f /var/spool/rsyslog/machine-0-%s", env.config.namespace()), fmt.Sprintf("ln -s %s/all-machines.log %s/", mcfg.LogDir, env.config.logDir()), fmt.Sprintf("ln -s %s/machine-0.log %s/", env.config.logDir(), mcfg.LogDir)) @@ -176,10 +184,11 @@ // // mcfg is supplied for testing purposes. var finishBootstrap = func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { - script, err := sshinit.ConfigureScript(cloudcfg) + configScript, err := sshinit.ConfigureScript(cloudcfg) if err != nil { return nil } + script := shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog) + configScript cmd := exec.Command("sudo", "/bin/bash", "-s") cmd.Stdin = strings.NewReader(script) cmd.Stdout = ctx.GetStdout() @@ -210,13 +219,17 @@ defer env.localMutex.Unlock() env.config = ecfg env.name = ecfg.Name() - + containerType := ecfg.container() + managerConfig := container.ManagerConfig{ + container.ConfigName: env.config.namespace(), + container.ConfigLogDir: env.config.logDir(), + } + if containerType == instance.LXC { + managerConfig["use-clone"] = strconv.FormatBool(env.config.lxcClone()) + managerConfig["use-aufs"] = strconv.FormatBool(env.config.lxcCloneAUFS()) + } env.containerManager, err = factory.NewContainerManager( - ecfg.container(), - container.ManagerConfig{ - Name: env.config.namespace(), - LogDir: env.config.logDir(), - }) + containerType, managerConfig) if err != nil { return err } @@ -272,7 +285,7 @@ // be synced and so forth without having a machine agent // running. func (env *localEnviron) setLocalStorage() error { - storage, err := filestorage.NewFileStorageWriter(env.config.storageDir(), filestorage.UseDefaultTmpDir) + storage, err := filestorage.NewFileStorageWriter(env.config.storageDir()) if err != nil { return err } @@ -281,24 +294,23 @@ } // StartInstance is specified in the InstanceBroker interface. -func (env *localEnviron) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (env *localEnviron) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { - series := possibleTools.OneSeries() - logger.Debugf("StartInstance: %q, %s", machineConfig.MachineId, series) - machineConfig.Tools = possibleTools[0] - machineConfig.MachineContainerType = env.config.container() - logger.Debugf("tools: %#v", machineConfig.Tools) + series := args.Tools.OneSeries() + logger.Debugf("StartInstance: %q, %s", args.MachineConfig.MachineId, series) + args.MachineConfig.Tools = args.Tools[0] + args.MachineConfig.MachineContainerType = env.config.container() + logger.Debugf("tools: %#v", args.MachineConfig.Tools) network := container.BridgeNetworkConfig(env.config.networkBridge()) - if err := environs.FinishMachineConfig(machineConfig, env.config.Config, cons); err != nil { + if err := environs.FinishMachineConfig(args.MachineConfig, env.config.Config, args.Constraints); err != nil { return nil, nil, err } // TODO: evaluate the impact of setting the contstraints on the // machineConfig for all machines rather than just state server nodes. // This limiation is why the constraints are assigned directly here. - machineConfig.Constraints = cons - machineConfig.AgentEnvironment[agent.Namespace] = env.config.namespace() - inst, hardware, err := env.containerManager.StartContainer(machineConfig, series, network) + args.MachineConfig.Constraints = args.Constraints + args.MachineConfig.AgentEnvironment[agent.Namespace] = env.config.namespace() + inst, hardware, err := env.containerManager.CreateContainer(args.MachineConfig, series, network) if err != nil { return nil, nil, err } @@ -311,7 +323,7 @@ if inst.Id() == bootstrapInstanceId { return fmt.Errorf("cannot stop the bootstrap instance") } - if err := env.containerManager.StopContainer(inst); err != nil { + if err := env.containerManager.DestroyContainer(inst); err != nil { return err } } @@ -374,38 +386,61 @@ // Destroy is specified in the Environ interface. func (env *localEnviron) Destroy() error { - // Kill all running instances. This must be done as - // root, or listing/stopping containers will fail. + // If bootstrap failed, for example because the user + // lacks sudo rights, then the agents won't have been + // installed. If that's the case, we can just remove + // the data-dir and exit. + agentsDir := filepath.Join(env.config.rootDir(), "agents") + if _, err := os.Stat(agentsDir); os.IsNotExist(err) { + // If we can't remove the root dir, then continue + // and attempt as root anyway. + if os.RemoveAll(env.config.rootDir()) == nil { + return nil + } + } if !checkIfRoot() { juju, err := exec.LookPath(os.Args[0]) if err != nil { return err } - args := []string{osenv.JujuHomeEnvKey + "=" + osenv.JujuHome()} - args = append(args, juju) - args = append(args, os.Args[1:]...) - args = append(args, "-y") + args := []string{ + osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(), + juju, "destroy-environment", "-y", "--force", env.Name(), + } cmd := exec.Command("sudo", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() - } else { - containers, err := env.containerManager.ListContainers() - if err != nil { + } + // Kill all running instances. This must be done as + // root, or listing/stopping containers will fail. + containers, err := env.containerManager.ListContainers() + if err != nil { + return err + } + for _, inst := range containers { + if err := env.containerManager.DestroyContainer(inst); err != nil { return err } - for _, inst := range containers { - if err := env.containerManager.StopContainer(inst); err != nil { - return err + } + cmd := exec.Command( + "pkill", + fmt.Sprintf("-%d", terminationworker.TerminationSignal), + "-f", filepath.Join(regexp.QuoteMeta(env.config.rootDir()), ".*", "jujud"), + ) + if err := cmd.Run(); err != nil { + if err, ok := err.(*exec.ExitError); ok { + // Exit status 1 means no processes were matched: + // we don't consider this an error here. + if err.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() != 1 { + return errgo.Annotate(err, "failed to kill jujud") } } - cmd := exec.Command( - "pkill", - fmt.Sprintf("-%d", terminationworker.TerminationSignal), - "-f", filepath.Join(regexp.QuoteMeta(env.config.rootDir()), ".*", "jujud"), - ) - return cmd.Run() } + if err := os.RemoveAll(env.config.rootDir()); err != nil && !os.IsNotExist(err) { + return err + } + return nil } // OpenPorts is specified in the Environ interface. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environprovider.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environprovider.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environprovider.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environprovider.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "os/user" "syscall" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" @@ -112,12 +112,14 @@ logger.Tracef("Look for proxies?") if cfg.HttpProxy() == "" && cfg.HttpsProxy() == "" && - cfg.FtpProxy() == "" { + cfg.FtpProxy() == "" && + cfg.NoProxy() == "" { proxy := osenv.DetectProxies() logger.Tracef("Proxies detected %#v", proxy) setIfNotBlank("http-proxy", proxy.Http) setIfNotBlank("https-proxy", proxy.Https) setIfNotBlank("ftp-proxy", proxy.Ftp) + setIfNotBlank("no-proxy", proxy.NoProxy) } if cfg.AptHttpProxy() == "" && cfg.AptHttpsProxy() == "" && @@ -178,15 +180,15 @@ localConfig := newEnvironConfig(cfg, validated) // Before potentially creating directories, make sure that the // root directory has not changed. + containerType := localConfig.container() if old != nil { oldLocalConfig, err := provider.newConfig(old) if err != nil { return nil, fmt.Errorf("old config is not a valid local config: %v", old) } - if localConfig.container() != oldLocalConfig.container() { + if containerType != oldLocalConfig.container() { return nil, fmt.Errorf("cannot change container from %q to %q", - oldLocalConfig.container(), - localConfig.container()) + oldLocalConfig.container(), containerType) } if localConfig.rootDir() != oldLocalConfig.rootDir() { return nil, fmt.Errorf("cannot change root-dir from %q to %q", @@ -205,8 +207,8 @@ } } // Currently only supported containers are "lxc" and "kvm". - if localConfig.container() != instance.LXC && localConfig.container() != instance.KVM { - return nil, fmt.Errorf("unsupported container type: %q", localConfig.container()) + if containerType != instance.LXC && containerType != instance.KVM { + return nil, fmt.Errorf("unsupported container type: %q", containerType) } dir, err := utils.NormalizePath(localConfig.rootDir()) if err != nil { @@ -218,6 +220,13 @@ // Always assign the normalized path. localConfig.attrs["root-dir"] = dir + if containerType != instance.KVM { + fastOptionAvailable := useFastLXC(containerType) + if _, found := localConfig.attrs["lxc-clone"]; !found { + localConfig.attrs["lxc-clone"] = fastOptionAvailable + } + } + // Apply the coerced unknown values back into the config. return cfg.Apply(localConfig.attrs) } @@ -228,17 +237,23 @@ # https://juju.ubuntu.com/docs/config-local.html local: type: local - # Override the directory that is used for the storage files and database. - # The default location is $JUJU_HOME/. - - # $JUJU_HOME defaults to ~/.juju + + # root-dir holds the directory that is used for the storage files and + # database. The default location is $JUJU_HOME/. + # $JUJU_HOME defaults to ~/.juju. Override if needed. + # # root-dir: ~/.juju/local - - # Override the storage port if you have multiple local providers, or if the - # default port is used by another program. + + # storage-port holds the port where the local provider starts the + # HTTP file server. Override the value if you have multiple local + # providers, or if the default port is used by another program. + # # storage-port: 8040 - - # Override the network bridge if you have changed the default lxc bridge + + # network-bridge holds the name of the LXC network bridge to use. + # Override if the default LXC network bridge is different. + # + # # network-bridge: lxcbr0 `[1:] diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environprovider_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environprovider_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environprovider_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environprovider_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,29 +7,31 @@ "errors" "os/user" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "github.com/juju/testing" gc "launchpad.net/gocheck" + "launchpad.net/juju-core/container/kvm" lxctesting "launchpad.net/juju-core/container/lxc/testing" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/provider" "launchpad.net/juju-core/provider/local" - "launchpad.net/juju-core/testing" - "launchpad.net/juju-core/testing/testbase" + coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/utils" ) type baseProviderSuite struct { lxctesting.TestSuite - home *testing.FakeHome + home *coretesting.FakeHome restore func() } func (s *baseProviderSuite) SetUpTest(c *gc.C) { s.TestSuite.SetUpTest(c) - s.home = testing.MakeFakeHomeNoEnvironments(c, "test") + s.home = coretesting.MakeFakeHomeNoEnvironments(c, "test") loggo.GetLogger("juju.provider.local").SetLogLevel(loggo.TRACE) s.restore = local.MockAddressForInterface() } @@ -41,7 +43,7 @@ } type prepareSuite struct { - testing.FakeHomeSuite + coretesting.FakeHomeSuite } var _ = gc.Suite(&prepareSuite{}) @@ -55,6 +57,8 @@ s.PatchEnvironment("HTTPS_PROXY", "") s.PatchEnvironment("ftp_proxy", "") s.PatchEnvironment("FTP_PROXY", "") + s.PatchEnvironment("no_proxy", "") + s.PatchEnvironment("NO_PROXY", "") s.HookCommandOutput(&utils.AptCommandOutput, nil, nil) s.PatchValue(local.CheckLocalPort, func(port int, desc string) error { return nil @@ -87,11 +91,13 @@ "http_proxy": "http://user@10.0.0.1", "HTTPS_PROXY": "https://user@10.0.0.1", "ftp_proxy": "ftp://user@10.0.0.1", + "no_proxy": "localhost,10.0.3.1", }, expectedProxy: osenv.ProxySettings{ - Http: "http://user@10.0.0.1", - Https: "https://user@10.0.0.1", - Ftp: "ftp://user@10.0.0.1", + Http: "http://user@10.0.0.1", + Https: "https://user@10.0.0.1", + Ftp: "ftp://user@10.0.0.1", + NoProxy: "localhost,10.0.3.1", }, expectedAptProxy: osenv.ProxySettings{ Http: "http://user@10.0.0.1", @@ -147,6 +153,19 @@ Ftp: "ftp://user@10.0.0.42", }, }, { + message: "skips proxy from environment if no-proxy set", + extraConfig: map[string]interface{}{ + "no-proxy": "localhost,10.0.3.1", + }, + env: map[string]string{ + "http_proxy": "http://user@10.0.0.1", + "HTTPS_PROXY": "https://user@10.0.0.1", + "ftp_proxy": "ftp://user@10.0.0.1", + }, + expectedProxy: osenv.ProxySettings{ + NoProxy: "localhost,10.0.3.1", + }, + }, { message: "apt-proxies detected", aptOutput: `CommandLine::AsString "apt-config dump"; Acquire::http::Proxy "10.0.3.1:3142"; @@ -205,23 +224,24 @@ c.Logf("\n%v: %s", i, test.message) cleanup := []func(){} for key, value := range test.env { - restore := testbase.PatchEnvironment(key, value) + restore := testing.PatchEnvironment(key, value) cleanup = append(cleanup, restore) } - _, restore := testbase.HookCommandOutput(&utils.AptCommandOutput, []byte(test.aptOutput), nil) + _, restore := testing.HookCommandOutput(&utils.AptCommandOutput, []byte(test.aptOutput), nil) cleanup = append(cleanup, restore) testConfig := baseConfig if test.extraConfig != nil { testConfig, err = baseConfig.Apply(test.extraConfig) c.Assert(err, gc.IsNil) } - env, err := provider.Prepare(testing.Context(c), testConfig) + env, err := provider.Prepare(coretesting.Context(c), testConfig) c.Assert(err, gc.IsNil) envConfig := env.Config() c.Assert(envConfig.HttpProxy(), gc.Equals, test.expectedProxy.Http) c.Assert(envConfig.HttpsProxy(), gc.Equals, test.expectedProxy.Https) c.Assert(envConfig.FtpProxy(), gc.Equals, test.expectedProxy.Ftp) + c.Assert(envConfig.NoProxy(), gc.Equals, test.expectedProxy.NoProxy) c.Assert(envConfig.AptHttpProxy(), gc.Equals, test.expectedAptProxy.Http) c.Assert(envConfig.AptHttpsProxy(), gc.Equals, test.expectedAptProxy.Https) @@ -269,7 +289,7 @@ s.PatchValue(local.UserCurrent, func() (*user.User, error) { return &user.User{Username: test.userOS}, test.userOSErr }) - env, err := provider.Prepare(testing.Context(c), basecfg) + env, err := provider.Prepare(coretesting.Context(c), basecfg) if test.err == "" { c.Assert(err, gc.IsNil) cfg := env.Config() @@ -279,3 +299,84 @@ } } } + +func (s *prepareSuite) TestFastLXCClone(c *gc.C) { + s.PatchValue(local.DetectAptProxies, func() (osenv.ProxySettings, error) { + return osenv.ProxySettings{}, nil + }) + s.PatchValue(&kvm.IsKVMSupported, func() (bool, error) { + return true, nil + }) + s.PatchValue(&local.VerifyPrerequisites, func(containerType instance.ContainerType) error { + return nil + }) + basecfg, err := config.New(config.UseDefaults, map[string]interface{}{ + "type": "local", + "name": "test", + }) + provider, err := environs.Provider("local") + c.Assert(err, gc.IsNil) + + type test struct { + systemDefault bool + extraConfig map[string]interface{} + expectClone bool + expectAUFS bool + } + tests := []test{{ + extraConfig: map[string]interface{}{ + "container": "lxc", + }, + }, { + extraConfig: map[string]interface{}{ + "container": "lxc", + "lxc-clone": "true", + }, + expectClone: true, + }, { + systemDefault: true, + extraConfig: map[string]interface{}{ + "container": "lxc", + }, + expectClone: true, + }, { + systemDefault: true, + extraConfig: map[string]interface{}{ + "container": "kvm", + }, + }, { + systemDefault: true, + extraConfig: map[string]interface{}{ + "container": "lxc", + "lxc-clone": false, + }, + }, { + systemDefault: true, + extraConfig: map[string]interface{}{ + "container": "lxc", + "lxc-clone-aufs": true, + }, + expectClone: true, + expectAUFS: true, + }} + + for i, test := range tests { + c.Logf("test %d: %v", i, test) + + releaseVersion := "12.04" + if test.systemDefault { + releaseVersion = "14.04" + } + s.PatchValue(local.ReleaseVersion, func() string { return releaseVersion }) + testConfig, err := basecfg.Apply(test.extraConfig) + c.Assert(err, gc.IsNil) + env, err := provider.Open(testConfig) + c.Assert(err, gc.IsNil) + localAttributes := env.Config().UnknownAttrs() + + value, _ := localAttributes["lxc-clone"].(bool) + c.Assert(value, gc.Equals, test.expectClone) + value, _ = localAttributes["lxc-clone-aufs"].(bool) + c.Assert(value, gc.Equals, test.expectAUFS) + } +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environ_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/environ_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,9 +9,9 @@ "path/filepath" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - "launchpad.net/juju-core/agent" coreCloudinit "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" @@ -20,12 +20,15 @@ "launchpad.net/juju-core/environs/jujutest" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/environs/tools" + "launchpad.net/juju-core/juju/arch" + "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/provider/local" - "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) +const echoCommandScript = "#!/bin/sh\necho $0 \"$@\" >> $0.args" + type environSuite struct { baseProviderSuite envtesting.ToolsFixture @@ -75,14 +78,26 @@ c.Assert(strings.Contains(url, "/tools"), jc.IsTrue) } +func (*environSuite) TestSupportedArchitectures(c *gc.C) { + testConfig := minimalConfig(c) + environ, err := local.Provider.Open(testConfig) + c.Assert(err, gc.IsNil) + c.Assert(err, gc.IsNil) + arches, err := environ.SupportedArchitectures() + c.Assert(err, gc.IsNil) + for _, a := range arches { + c.Assert(arch.IsSupportedArch(a), jc.IsTrue) + } +} + type localJujuTestSuite struct { baseProviderSuite jujutest.Tests restoreRootCheck func() oldUpstartLocation string - oldPath string testPath string dbServiceName string + fakesudo string } func (s *localJujuTestSuite) SetUpTest(c *gc.C) { @@ -90,10 +105,14 @@ // Construct the directories first. err := local.CreateDirs(c, minimalConfig(c)) c.Assert(err, gc.IsNil) - s.oldPath = os.Getenv("PATH") s.testPath = c.MkDir() + s.fakesudo = filepath.Join(s.testPath, "sudo") s.PatchEnvPathPrepend(s.testPath) + // Write a fake "sudo" which records its args to sudo.args. + err = ioutil.WriteFile(s.fakesudo, []byte(echoCommandScript), 0755) + c.Assert(err, gc.IsNil) + // Add in an admin secret s.Tests.TestConfig["admin-secret"] = "sekrit" s.restoreRootCheck = local.SetRootCheckFunction(func() bool { return false }) @@ -131,6 +150,22 @@ }, }) +func (s *localJujuTestSuite) TestStartStop(c *gc.C) { + c.Skip("StartInstance not implemented yet.") +} + +func (s *localJujuTestSuite) testBootstrap(c *gc.C) (env environs.Environ) { + testConfig := minimalConfig(c) + ctx := coretesting.Context(c) + environ, err := local.Provider.Prepare(ctx, testConfig) + c.Assert(err, gc.IsNil) + envtesting.UploadFakeTools(c, environ.Storage()) + defer environ.Storage().RemoveAll() + err = environ.Bootstrap(ctx, constraints.Value{}) + c.Assert(err, gc.IsNil) + return environ +} + func (s *localJujuTestSuite) TestBootstrap(c *gc.C) { s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { c.Assert(cloudcfg.AptUpdate(), jc.IsFalse) @@ -138,21 +173,46 @@ c.Assert(cloudcfg.Packages(), gc.HasLen, 0) c.Assert(mcfg.AgentEnvironment, gc.Not(gc.IsNil)) // local does not allow machine-0 to host units - bootstrapJobs, err := agent.UnmarshalBootstrapJobs(mcfg.AgentEnvironment[agent.BootstrapJobs]) - c.Assert(err, gc.IsNil) - c.Assert(bootstrapJobs, gc.DeepEquals, []state.MachineJob{state.JobManageEnviron}) + c.Assert(mcfg.Jobs, gc.DeepEquals, []params.MachineJob{params.JobManageEnviron}) return nil }) - testConfig := minimalConfig(c) - ctx := coretesting.Context(c) - environ, err := local.Provider.Prepare(ctx, testConfig) - c.Assert(err, gc.IsNil) - envtesting.UploadFakeTools(c, environ.Storage()) - defer environ.Storage().RemoveAll() - err = environ.Bootstrap(ctx, constraints.Value{}) + s.testBootstrap(c) +} + +func (s *localJujuTestSuite) TestDestroy(c *gc.C) { + s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { + return nil + }) + env := s.testBootstrap(c) + err := env.Destroy() + // Succeeds because there's no "agents" directory, + // so destroy will just return without attempting + // sudo or anything. c.Assert(err, gc.IsNil) + c.Assert(s.fakesudo+".args", jc.DoesNotExist) } -func (s *localJujuTestSuite) TestStartStop(c *gc.C) { - c.Skip("StartInstance not implemented yet.") +func (s *localJujuTestSuite) TestDestroyCallSudo(c *gc.C) { + s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { + return nil + }) + env := s.testBootstrap(c) + rootDir := env.Config().AllAttrs()["root-dir"].(string) + agentsDir := filepath.Join(rootDir, "agents") + err := os.Mkdir(agentsDir, 0755) + c.Assert(err, gc.IsNil) + err = env.Destroy() + c.Assert(err, gc.IsNil) + data, err := ioutil.ReadFile(s.fakesudo + ".args") + c.Assert(err, gc.IsNil) + expected := []string{ + s.fakesudo, + "JUJU_HOME=" + osenv.JujuHome(), + os.Args[0], + "destroy-environment", + "-y", + "--force", + env.Config().Name(), + } + c.Assert(string(data), gc.Equals, strings.Join(expected, " ")+"\n") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -3,17 +3,19 @@ package local import ( + "github.com/juju/testing" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/config" - "launchpad.net/juju-core/testing/testbase" ) var ( - Provider = providerInstance - FinishBootstrap = &finishBootstrap CheckLocalPort = &checkLocalPort DetectAptProxies = &detectAptProxies + FinishBootstrap = &finishBootstrap + Provider = providerInstance + ReleaseVersion = &releaseVersion + UseFastLXC = useFastLXC UserCurrent = &userCurrent ) @@ -53,7 +55,7 @@ // MockAddressForInterface replaces the getAddressForInterface with a function // that returns a constant localhost ip address. func MockAddressForInterface() func() { - return testbase.PatchValue(&getAddressForInterface, func(name string) (string, error) { + return testing.PatchValue(&getAddressForInterface, func(name string) (string, error) { logger.Debugf("getAddressForInterface called for %s", name) return "127.0.0.1", nil }) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/lxc.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/lxc.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/lxc.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/lxc.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,30 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local + +import ( + "strconv" + + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/version" +) + +// releaseVersion is a function that returns a string representing the +// DISTRIB_RELEASE from the /etc/lsb-release file. +var releaseVersion = version.ReleaseVersion + +func useFastLXC(containerType instance.ContainerType) bool { + if containerType != instance.LXC { + return false + } + release := releaseVersion() + if release == "" { + return false + } + value, err := strconv.ParseFloat(release, 64) + if err != nil { + return false + } + return value >= 14.04 +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/lxc_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/lxc_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/lxc_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/lxc_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,53 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package local_test + +import ( + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/provider/local" + "launchpad.net/juju-core/testing/testbase" +) + +type lxcTest struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&lxcTest{}) + +func (*lxcTest) TestUseFastLXCForContainer(c *gc.C) { + c.Assert(local.UseFastLXC(instance.ContainerType("")), jc.IsFalse) + c.Assert(local.UseFastLXC(instance.KVM), jc.IsFalse) +} + +func (t *lxcTest) TestUseFastLXC(c *gc.C) { + for i, test := range []struct { + message string + releaseVersion string + expected bool + }{{ + message: "missing release file", + }, { + message: "precise release", + releaseVersion: "12.04", + }, { + message: "trusty release", + releaseVersion: "14.04", + expected: true, + }, { + message: "unstable unicorn", + releaseVersion: "14.10", + expected: true, + }, { + message: "jaunty", + releaseVersion: "9.10", + }} { + c.Logf("%v: %v", i, test.message) + t.PatchValue(local.ReleaseVersion, func() string { return test.releaseVersion }) + value := local.UseFastLXC(instance.LXC) + c.Assert(value, gc.Equals, test.expected) + } +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/prereqs.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/prereqs.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/prereqs.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/prereqs.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,14 @@ import ( "errors" "fmt" + "os" "os/exec" "regexp" "runtime" + "launchpad.net/juju-core/agent/mongo" "launchpad.net/juju-core/container/kvm" "launchpad.net/juju-core/instance" - "launchpad.net/juju-core/upstart" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -36,9 +37,19 @@ const installLxcUbuntu = ` Linux Containers (LXC) userspace tools must be installed to enable the local provider: - + sudo apt-get install lxc` +const installRsyslogGnutlsUbuntu = ` +rsyslog-gnutls must be installed to enable the local provider: + + sudo apt-get install rsyslog-gnutls` + +const installRsyslogGnutlsGeneric = ` +rsyslog-gnutls must be installed to enable the local provider. +Please consult your operating system distribution's documentation +for instructions on installing this package.` + const installLxcGeneric = ` Linux Containers (LXC) userspace tools must be installed to enable the local provider. Please consult your operating system distribution's @@ -56,6 +67,14 @@ // unit testing. var lxclsPath = "lxc-ls" +// isPackageInstalled is a variable to support testing. +var isPackageInstalled = utils.IsPackageInstalled + +// defaultRsyslogGnutlsPath is the default path to the +// rsyslog GnuTLS module. This is a variable only to +// support unit testing. +var defaultRsyslogGnutlsPath = "/usr/lib/rsyslog/lmnsd_gtls.so" + // The operating system the process is running in. // This is a variable only to support unit testing. var goos = runtime.GOOS @@ -66,13 +85,16 @@ // VerifyPrerequisites verifies the prerequisites of // the local machine (machine 0) for running the local // provider. -func VerifyPrerequisites(containerType instance.ContainerType) error { +var VerifyPrerequisites = func(containerType instance.ContainerType) error { if goos != "linux" { return fmt.Errorf(errUnsupportedOS, goos) } if err := verifyMongod(); err != nil { return err } + if err := verifyRsyslogGnutls(); err != nil { + return err + } switch containerType { case instance.LXC: return verifyLxc() @@ -83,7 +105,10 @@ } func verifyMongod() error { - path := upstart.MongodPath() + path, err := mongo.MongodPath() + if err != nil { + return wrapMongodNotExist(err) + } ver, err := mongodVersion(path) if err != nil { @@ -123,6 +148,23 @@ return nil } +func verifyRsyslogGnutls() error { + if isPackageInstalled("rsyslog-gnutls") { + return nil + } + if utils.IsUbuntu() { + return errors.New(installRsyslogGnutlsUbuntu) + } + // Not all Linuxes will distribute the module + // in the same way. Check if it's in the default + // location too. + _, err := os.Stat(defaultRsyslogGnutlsPath) + if err == nil { + return nil + } + return fmt.Errorf("%v\n%s", err, installRsyslogGnutlsGeneric) +} + func wrapMongodNotExist(err error) error { if utils.IsUbuntu() { series := version.Current.Series diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/prereqs_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/prereqs_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/local/prereqs_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/prereqs_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,9 +11,10 @@ gc "launchpad.net/gocheck" + "launchpad.net/juju-core/agent/mongo" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/testing/testbase" - "launchpad.net/juju-core/upstart" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -36,26 +37,33 @@ // even when mongodb and lxc-ls can't be // found. lxclsPath = "/bin/true" + + // Allow non-prereq tests to pass by default. + isPackageInstalled = func(packageName string) bool { + return true + } } func (s *prereqsSuite) SetUpTest(c *gc.C) { s.LoggingSuite.SetUpTest(c) s.tmpdir = c.MkDir() s.testMongodPath = filepath.Join(s.tmpdir, "mongod") - lxclsPath = filepath.Join(s.tmpdir, "lxc-ls") + s.PatchEnvironment("PATH", s.tmpdir) - upstart.JujuMongodPath = "/somewhere/that/doesnt/exist" + s.PatchValue(&mongo.JujuMongodPath, "/somewhere/that/wont/exist") + + s.setMongoVersion(c, lowestMongoVersion.Major, lowestMongoVersion.Minor, lowestMongoVersion.Patch) os.Setenv("JUJUTEST_LSB_RELEASE_ID", "Ubuntu") err := ioutil.WriteFile(filepath.Join(s.tmpdir, "lsb_release"), []byte(lsbrelease), 0777) c.Assert(err, gc.IsNil) -} -func (s *prereqsSuite) TearDownTest(c *gc.C) { - s.testMongodPath = "" - lxclsPath = "/bin/true" - s.LoggingSuite.TearDownTest(c) + // symlink $temp/dpkg-query to /bin/true, to + // simulate package installation query responses. + err = os.Symlink("/bin/true", filepath.Join(s.tmpdir, "dpkg-query")) + c.Assert(err, gc.IsNil) + s.PatchValue(&isPackageInstalled, utils.IsPackageInstalled) } func (*prereqsSuite) TestSupportedOS(c *gc.C) { @@ -72,17 +80,14 @@ echo Thu Feb 13 15:53:58.210 git version: b9925db5eac369d77a3a5f5d98a145eaaacd9673 ` -func (s *prereqsSuite) setMongoVersion(major, minor, patch int) { +func (s *prereqsSuite) setMongoVersion(c *gc.C, major, minor, patch int) { script := fmt.Sprintf(fakeMongoFmt, major, minor, patch) err := ioutil.WriteFile(s.testMongodPath, []byte(script), 0777) - - if err != nil { - panic(err) - } + c.Assert(err, gc.IsNil) } func (s *prereqsSuite) TestParseMongoVersion(c *gc.C) { - s.setMongoVersion(2, 2, 2) + s.setMongoVersion(c, 2, 2, 2) ver, err := mongodVersion(s.testMongodPath) c.Assert(err, gc.IsNil) @@ -93,28 +98,28 @@ lowver := version.Number{2, 2, 2, 0} s.PatchValue(&lowestMongoVersion, lowver) - s.setMongoVersion(3, 0, 0) + s.setMongoVersion(c, 3, 0, 0) c.Assert(verifyMongod(), gc.IsNil) - s.setMongoVersion(2, 3, 0) + s.setMongoVersion(c, 2, 3, 0) c.Assert(verifyMongod(), gc.IsNil) - s.setMongoVersion(2, 2, 3) + s.setMongoVersion(c, 2, 2, 3) c.Assert(verifyMongod(), gc.IsNil) - s.setMongoVersion(2, 2, 2) + s.setMongoVersion(c, 2, 2, 2) c.Assert(verifyMongod(), gc.IsNil) expected := fmt.Sprintf("installed version of mongod .* is not supported by Juju. "+ "Juju requires version %v or greater.", lowver) - s.setMongoVersion(2, 2, 1) + s.setMongoVersion(c, 2, 2, 1) c.Assert(verifyMongod(), gc.ErrorMatches, expected) - s.setMongoVersion(2, 1, 3) + s.setMongoVersion(c, 2, 1, 3) c.Assert(verifyMongod(), gc.ErrorMatches, expected) - s.setMongoVersion(1, 3, 3) + s.setMongoVersion(c, 1, 3, 3) c.Assert(verifyMongod(), gc.ErrorMatches, expected) } @@ -134,7 +139,10 @@ } func (s *prereqsSuite) TestMongoPrereq(c *gc.C) { - err := VerifyPrerequisites(instance.LXC) + err := os.Remove(s.testMongodPath) + c.Assert(err, gc.IsNil) + + err = VerifyPrerequisites(instance.LXC) c.Assert(err, gc.ErrorMatches, "(.|\n)*MongoDB server must be installed(.|\n)*") c.Assert(err, gc.ErrorMatches, "(.|\n)*apt-get install mongodb-server(.|\n)*") @@ -144,16 +152,13 @@ c.Assert(err, gc.Not(gc.ErrorMatches), "(.|\n)*apt-get install(.|\n)*") s.PatchValue(&lowestMongoVersion, version.Number{2, 2, 2, 0}) - s.setMongoVersion(3, 0, 0) - err = ioutil.WriteFile(filepath.Join(s.tmpdir, "lxc-ls"), nil, 0777) - c.Assert(err, gc.IsNil) + s.setMongoVersion(c, 3, 0, 0) err = VerifyPrerequisites(instance.LXC) c.Assert(err, gc.IsNil) } func (s *prereqsSuite) TestLxcPrereq(c *gc.C) { - s.PatchValue(&lowestMongoVersion, version.Number{2, 2, 2, 0}) - s.setMongoVersion(3, 0, 0) + s.PatchValue(&lxclsPath, filepath.Join(s.tmpdir, "non-existent")) err := VerifyPrerequisites(instance.LXC) c.Assert(err, gc.ErrorMatches, "(.|\n)*Linux Containers \\(LXC\\) userspace tools must be\ninstalled(.|\n)*") @@ -168,4 +173,26 @@ c.Assert(err, gc.IsNil) err = VerifyPrerequisites(instance.LXC) c.Assert(err, gc.IsNil) +} + +func (s *prereqsSuite) TestRsyslogGnutlsPrereq(c *gc.C) { + err := os.Remove(filepath.Join(s.tmpdir, "dpkg-query")) + c.Assert(err, gc.IsNil) + err = os.Symlink("/bin/false", filepath.Join(s.tmpdir, "dpkg-query")) + c.Assert(err, gc.IsNil) + + err = VerifyPrerequisites(instance.LXC) + c.Assert(err, gc.ErrorMatches, "(.|\n)*rsyslog-gnutls must be installed to enable the local provider(.|\n)*") + c.Assert(err, gc.ErrorMatches, "(.|\n)*apt-get install rsyslog-gnutls(.|\n)*") + + s.PatchValue(&defaultRsyslogGnutlsPath, filepath.Join(s.tmpdir, "non-existent")) + os.Setenv("JUJUTEST_LSB_RELEASE_ID", "NotUbuntu") + err = VerifyPrerequisites(instance.LXC) + c.Assert(err, gc.ErrorMatches, "(.|\n)*non-existent: no such file or directory(.|\n)*") + c.Assert(err, gc.Not(gc.ErrorMatches), "(.|\n)*apt-get install rsyslog-gnutls(.|\n)*") + + err = ioutil.WriteFile(defaultRsyslogGnutlsPath, nil, 0644) + c.Assert(err, gc.IsNil) + err = VerifyPrerequisites(instance.LXC) + c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,7 +16,6 @@ "launchpad.net/juju-core/agent" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" @@ -48,6 +47,12 @@ type maasEnviron struct { name string + // archMutex gates access to supportedArchitectures + archMutex sync.Mutex + // supportedArchitectures caches the architectures + // for which images can be instantiated. + supportedArchitectures []string + // ecfgMutex protects the *Unlocked fields below. ecfgMutex sync.Mutex @@ -131,6 +136,22 @@ return nil } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (env *maasEnviron) SupportedArchitectures() ([]string, error) { + env.archMutex.Lock() + defer env.archMutex.Unlock() + if env.supportedArchitectures != nil { + return env.supportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + Stream: env.Config().ImageStream(), + }) + var err error + env.supportedArchitectures, err = common.SupportedArchitectures(env, imageConstraint) + return env.supportedArchitectures, err +} + // getMAASClient returns a MAAS client object to use for a request, in a // lock-protected fashion. func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject { @@ -168,9 +189,27 @@ return params } +// addNetworks converts networks include/exclude information into +// url.Values object suitable to pass to MAAS when acquiring a node. +func addNetworks(params url.Values, nets environs.Networks) { + // Network Inclusion/Exclusion setup + if nets.IncludedNetworks != nil { + for _, network_name := range nets.IncludedNetworks { + params.Add("networks", network_name) + } + } + if nets.ExcludedNetworks != nil { + for _, not_network_name := range nets.ExcludedNetworks { + params.Add("not_networks", not_network_name) + } + } + +} + // acquireNode allocates a node from the MAAS. -func (environ *maasEnviron) acquireNode(cons constraints.Value, possibleTools tools.List) (gomaasapi.MAASObject, *tools.Tools, error) { +func (environ *maasEnviron) acquireNode(cons constraints.Value, nets environs.Networks, possibleTools tools.List) (gomaasapi.MAASObject, *tools.Tools, error) { acquireParams := convertConstraints(cons) + addNetworks(acquireParams, nets) acquireParams.Add("agent_name", environ.ecfg().maasAgentName()) var result gomaasapi.JSONObject var err error @@ -230,16 +269,15 @@ } // StartInstance is specified in the InstanceBroker interface. -func (environ *maasEnviron) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { var inst *maasInstance var err error - if node, tools, err := environ.acquireNode(cons, possibleTools); err != nil { + if node, tools, err := environ.acquireNode(args.Constraints, args.Networks, args.Tools); err != nil { return nil, nil, fmt.Errorf("cannot run instances: %v", err) } else { inst = &maasInstance{maasObject: &node, environ: environ} - machineConfig.Tools = tools + args.MachineConfig.Tools = tools } defer func() { if err != nil { @@ -253,32 +291,25 @@ if err != nil { return nil, nil, err } - info := machineInfo{hostname} - runCmd, err := info.cloudinitRunCmd() + additionalScripts, err := additionalScripts(hostname) if err != nil { return nil, nil, err } - if err := environs.FinishMachineConfig(machineConfig, environ.Config(), cons); err != nil { + if err := environs.FinishMachineConfig(args.MachineConfig, environ.Config(), args.Constraints); err != nil { return nil, nil, err } // TODO(thumper): 2013-08-28 bug 1217614 // The machine envronment config values are being moved to the agent config. // Explicitly specify that the lxc containers use the network bridge defined above. - machineConfig.AgentEnvironment[agent.LxcBridge] = "br0" - userdata, err := environs.ComposeUserData( - machineConfig, - runCmd, - createBridgeNetwork(), - linkBridgeInInterfaces(), - "service networking restart", - ) + args.MachineConfig.AgentEnvironment[agent.LxcBridge] = "br0" + userdata, err := environs.ComposeUserData(args.MachineConfig, additionalScripts...) if err != nil { msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err) return nil, nil, msg } logger.Debugf("maas user data; %d bytes", len(userdata)) - series := possibleTools.OneSeries() + series := args.Tools.OneSeries() if err := environ.startNode(*inst.maasObject, series, userdata); err != nil { return nil, nil, err } @@ -287,6 +318,25 @@ return inst, nil, nil } +// additionalScripts is an additional set of commands +// to run during cloud-init (before the synchronous phase). +func additionalScripts(hostname string) ([]string, error) { + info := machineInfo{hostname} + runCmd, err := info.cloudinitRunCmd() + if err != nil { + return nil, err + } + return []string{ + runCmd, + utils.CommandString(utils.AptGetCommand("update")...), + utils.CommandString(utils.AptGetCommand("install", "bridge-utils")...), + "ifdown eth0", + createBridgeNetwork(), + linkBridgeInInterfaces(), + "ifup br0", + }, nil +} + // StartInstance is specified in the InstanceBroker interface. func (environ *maasEnviron) StopInstances(instances []instance.Instance) error { // Shortcut to exit quickly if 'instances' is an empty slice or nil. @@ -421,12 +471,12 @@ func (e *maasEnviron) GetImageSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseImagesPath)}, nil + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)}, nil } // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. func (e *maasEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off the control bucket. return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environprovider.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environprovider.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environprovider.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environprovider.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "errors" "os" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" @@ -64,12 +64,14 @@ # https://juju.ubuntu.com/docs/config-maas.html maas: type: maas - + # maas-server specifies the location of the MAAS server. It must # specify the base path. + # maas-server: 'http://192.168.1.1/MAAS/' - + # maas-oauth holds the OAuth credentials from MAAS. + # maas-oauth: '' `[1:] diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environprovider_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environprovider_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environprovider_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environprovider_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( "io/ioutil" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/juju/osenv" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -56,8 +56,6 @@ s.LoggingSuite.TearDownSuite(c) } -var _ = gc.Suite(&environSuite{}) - func getSimpleTestConfig(c *gc.C, extraAttrs coretesting.Attrs) *config.Config { attrs := coretesting.FakeConfig() attrs["type"] = "maas" @@ -195,3 +193,18 @@ c.Check(err, gc.IsNil) c.Check(env.Name(), gc.Equals, "testenv") } + +func (*environSuite) TestAdditionalSCripts(c *gc.C) { + const aptGetPrefix = "env DEBIAN_FRONTEND=noninteractive apt-get --option=Dpkg::Options::=--force-confold --option=Dpkg::options::=--force-unsafe-io --assume-yes --quiet" + scripts, err := maas.AdditionalScripts("testing.invalid") + c.Assert(err, gc.IsNil) + c.Assert(scripts, gc.DeepEquals, []string{ + "mkdir -p '/var/lib/juju'; echo -n 'hostname: testing.invalid\n' > '/var/lib/juju/MAASmachine.txt'", + aptGetPrefix + " update", + aptGetPrefix + " install bridge-utils", + "ifdown eth0", + "cat > /etc/network/eth0.config << EOF\niface eth0 inet manual\n\nauto br0\niface br0 inet dhcp\n bridge_ports eth0\nEOF\n", + `sed -i "s/iface eth0 inet dhcp/source \/etc\/network\/eth0.config/" /etc/network/interfaces`, + "ifup br0", + }) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,7 +9,9 @@ "fmt" "io/ioutil" "net/url" + "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -26,7 +28,6 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -222,7 +223,8 @@ env := suite.makeEnviron() suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) - _, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools}) + _, _, err := env.acquireNode(constraints.Value{}, environs.Networks{}, + tools.List{fakeTools}) c.Check(err, gc.IsNil) operations := suite.testMAASObject.TestServer.NodeOperations() @@ -238,7 +240,8 @@ suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) constraints := constraints.Value{Arch: stringp("arm"), Mem: uint64p(1024)} - _, _, err := env.acquireNode(constraints, tools.List{fakeTools}) + _, _, err := env.acquireNode(constraints, environs.Networks{}, + tools.List{fakeTools}) c.Check(err, gc.IsNil) requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() @@ -254,7 +257,8 @@ env := suite.makeEnviron() suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) - _, _, err := env.acquireNode(constraints.Value{}, tools.List{fakeTools}) + _, _, err := env.acquireNode(constraints.Value{}, environs.Networks{}, + tools.List{fakeTools}) c.Check(err, gc.IsNil) requestValues := suite.testMAASObject.TestServer.NodeOperationRequestValues() @@ -263,26 +267,69 @@ c.Assert(nodeRequestValues[0].Get("agent_name"), gc.Equals, exampleAgentName) } +var testValues = []struct { + constraints constraints.Value + expectedResult url.Values +}{ + {constraints.Value{Arch: stringp("arm")}, url.Values{"arch": {"arm"}}}, + {constraints.Value{CpuCores: uint64p(4)}, url.Values{"cpu_count": {"4"}}}, + {constraints.Value{Mem: uint64p(1024)}, url.Values{"mem": {"1024"}}}, + + // CpuPower is ignored. + {constraints.Value{CpuPower: uint64p(1024)}, url.Values{}}, + + // RootDisk is ignored. + {constraints.Value{RootDisk: uint64p(8192)}, url.Values{}}, + {constraints.Value{Tags: &[]string{"foo", "bar"}}, url.Values{"tags": {"foo,bar"}}}, + {constraints.Value{Arch: stringp("arm"), CpuCores: uint64p(4), Mem: uint64p(1024), CpuPower: uint64p(1024), RootDisk: uint64p(8192), Tags: &[]string{"foo", "bar"}}, url.Values{"arch": {"arm"}, "cpu_count": {"4"}, "mem": {"1024"}, "tags": {"foo,bar"}}}, +} + func (*environSuite) TestConvertConstraints(c *gc.C) { - var testValues = []struct { - constraints constraints.Value - expectedResult url.Values - }{ - {constraints.Value{Arch: stringp("arm")}, url.Values{"arch": {"arm"}}}, - {constraints.Value{CpuCores: uint64p(4)}, url.Values{"cpu_count": {"4"}}}, - {constraints.Value{Mem: uint64p(1024)}, url.Values{"mem": {"1024"}}}, - // CpuPower is ignored. - {constraints.Value{CpuPower: uint64p(1024)}, url.Values{}}, - // RootDisk is ignored. - {constraints.Value{RootDisk: uint64p(8192)}, url.Values{}}, - {constraints.Value{Tags: &[]string{"foo", "bar"}}, url.Values{"tags": {"foo,bar"}}}, - {constraints.Value{Arch: stringp("arm"), CpuCores: uint64p(4), Mem: uint64p(1024), CpuPower: uint64p(1024), RootDisk: uint64p(8192), Tags: &[]string{"foo", "bar"}}, url.Values{"arch": {"arm"}, "cpu_count": {"4"}, "mem": {"1024"}, "tags": {"foo,bar"}}}, - } for _, test := range testValues { c.Check(convertConstraints(test.constraints), gc.DeepEquals, test.expectedResult) } } +var testNetworkValues = []struct { + networks environs.Networks + expectedResult url.Values +}{ + { + environs.Networks{}, + url.Values{}, + }, + { + environs.Networks{ + IncludedNetworks: []string{"included_net_1"}, + }, + url.Values{"networks": {"included_net_1"}}, + }, + { + environs.Networks{ + ExcludedNetworks: []string{"excluded_net_1"}, + }, + url.Values{"not_networks": {"excluded_net_1"}}, + }, + { + environs.Networks{ + IncludedNetworks: []string{"included_net_1", "included_net_2"}, + ExcludedNetworks: []string{"excluded_net_1", "excluded_net_2"}, + }, + url.Values{ + "networks": {"included_net_1", "included_net_2"}, + "not_networks": {"excluded_net_1", "excluded_net_2"}, + }, + }, +} + +func (*environSuite) TestConvertNetworks(c *gc.C) { + for _, test := range testNetworkValues { + var vals = url.Values{} + addNetworks(vals, test.networks) + c.Check(vals, gc.DeepEquals, test.expectedResult) + } +} + func (suite *environSuite) getInstance(systemId string) *maasInstance { input := `{"system_id": "` + systemId + `"}` node := suite.testMAASObject.TestServer.NewNode(input) @@ -385,7 +432,10 @@ err = env.SetConfig(cfg) c.Assert(err, gc.IsNil) err = bootstrap.Bootstrap(coretesting.Context(c), env, constraints.Value{}) - c.Check(err, gc.ErrorMatches, "cannot find bootstrap tools.*") + stripped := strings.Replace(err.Error(), "\n", "", -1) + c.Check(stripped, + gc.Matches, + "cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*") } func (suite *environSuite) TestBootstrapFailsIfNoNodes(c *gc.C) { @@ -467,3 +517,10 @@ c.Assert(len(sources), gc.Equals, 1) assertSourceContents(c, sources[0], "filename", data) } + +func (suite *environSuite) TestSupportedArchitectures(c *gc.C) { + env := suite.makeEnviron() + a, err := env.SupportedArchitectures() + c.Assert(err, gc.IsNil) + c.Assert(a, gc.DeepEquals, []string{"amd64"}) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,8 +10,9 @@ ) var ( - ShortAttempt = &shortAttempt - APIVersion = apiVersion + ShortAttempt = &shortAttempt + APIVersion = apiVersion + AdditionalScripts = additionalScripts ) func MAASAgentName(env environs.Environ) string { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/storage_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/storage_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/maas/storage_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/storage_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,12 +12,12 @@ "net/url" "sync" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/gomaasapi" "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/errors" - jc "launchpad.net/juju-core/testing/checkers" ) type storageSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,11 +7,11 @@ "fmt" "regexp" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs/config" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,11 +12,11 @@ "strings" "sync" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/httpstorage" "launchpad.net/juju-core/environs/manual" @@ -28,16 +28,12 @@ "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" - "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils/ssh" "launchpad.net/juju-core/worker/localstorage" "launchpad.net/juju-core/worker/terminationworker" ) const ( - // TODO(axw) make this configurable? - dataDir = "/var/lib/juju" - // storageSubdir is the subdirectory of // dataDir in which storage will be located. storageSubdir = "storage" @@ -63,7 +59,7 @@ var errNoStartInstance = errors.New("manual provider cannot start instances") var errNoStopInstance = errors.New("manual provider cannot stop instances") -func (*manualEnviron) StartInstance(constraints.Value, tools.List, *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (*manualEnviron) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { return nil, nil, errNoStartInstance } @@ -90,6 +86,17 @@ return e.envConfig().Name() } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (e *manualEnviron) SupportedArchitectures() ([]string, error) { + envConfig := e.envConfig() + host := envConfig.bootstrapHost() + hc, _, err := manual.DetectSeriesAndHardwareCharacteristics(host) + if err != nil { + return nil, err + } + return []string{*hc.Arch}, nil +} + func (e *manualEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { // Set "use-sshstorage" to false, so agents know not to use sshstorage. cfg, err := e.Config().Apply(map[string]interface{}{"use-sshstorage": false}) @@ -112,7 +119,7 @@ return manual.Bootstrap(manual.BootstrapArgs{ Context: ctx, Host: host, - DataDir: dataDir, + DataDir: agent.DefaultDataDir, Environ: e, PossibleTools: selectedTools, Series: series, @@ -141,7 +148,7 @@ var stor storage.Storage if envConfig.useSSHStorage() { storageDir := e.StorageDir() - storageTmpdir := path.Join(dataDir, storageTmpSubdir) + storageTmpdir := path.Join(agent.DefaultDataDir, storageTmpSubdir) stor, err = newSSHStorage("ubuntu@"+e.cfg.bootstrapHost(), storageDir, storageTmpdir) if err != nil { return fmt.Errorf("initialising SSH storage failed: %v", err) @@ -188,6 +195,7 @@ } var newSSHStorage = func(sshHost, storageDir, storageTmpdir string) (storage.Storage, error) { + logger.Debugf("using ssh storage at host %q dir %q", sshHost, storageDir) return sshstorage.NewSSHStorage(sshstorage.NewSSHStorageParams{ Host: sshHost, StorageDir: storageDir, @@ -200,7 +208,7 @@ func (e *manualEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { // Add the simplestreams source off private storage. return []simplestreams.DataSource{ - storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath), + storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath), }, nil } @@ -256,7 +264,7 @@ } func (e *manualEnviron) StorageDir() string { - return path.Join(dataDir, storageSubdir) + return path.Join(agent.DefaultDataDir, storageSubdir) } func (e *manualEnviron) SharedStorageAddr() string { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/environ_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/environ_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "errors" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -14,7 +15,6 @@ "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -129,3 +129,13 @@ c.Assert(err, gc.IsNil) c.Assert(strings.Contains(url, "/tools"), jc.IsTrue) } + +func (s *environSuite) TestSupportedArchitectures(c *gc.C) { + s.PatchValue(&manual.DetectSeriesAndHardwareCharacteristics, func(host string) (instance.HardwareCharacteristics, string, error) { + c.Assert(host, gc.Equals, "hostname") + return instance.MustParseHardware("arch=arm64"), "precise", nil + }) + a, err := s.env.SupportedArchitectures() + c.Assert(err, gc.IsNil) + c.Assert(a, gc.DeepEquals, []string{"arm64"}) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/provider_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/provider_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/manual/provider_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/provider_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "io" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -14,7 +15,6 @@ "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/provider/manual" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/config.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/config.go 2014-03-20 12:52:38.000000000 +0000 @@ -198,17 +198,18 @@ // Check for deprecated fields and log a warning. We also print to stderr to ensure the user sees the message // even if they are not running with --debug. - if defaultImageId := cfg.AllAttrs()["default-image-id"]; defaultImageId != nil && defaultImageId.(string) != "" { + cfgAttrs := cfg.AllAttrs() + if defaultImageId := cfgAttrs["default-image-id"]; defaultImageId != nil && defaultImageId.(string) != "" { msg := fmt.Sprintf( "Config attribute %q (%v) is deprecated and ignored.\n"+ "Your cloud provider should have set up image metadata to provide the correct image id\n"+ "for your chosen series and archietcure. If this is a private Openstack deployment without\n"+ - "existing image metadata, please run 'juju help image-metadata' to see how suitable image"+ + "existing image metadata, please run 'juju-metadata help' to see how suitable image"+ "metadata can be generated.", "default-image-id", defaultImageId) logger.Warningf(msg) } - if defaultInstanceType := cfg.AllAttrs()["default-instance-type"]; defaultInstanceType != nil && defaultInstanceType.(string) != "" { + if defaultInstanceType := cfgAttrs["default-instance-type"]; defaultInstanceType != nil && defaultInstanceType.(string) != "" { msg := fmt.Sprintf( "Config attribute %q (%v) is deprecated and ignored.\n"+ "The correct instance flavor is determined using constraints, globally specified\n"+ @@ -217,7 +218,13 @@ "default-instance-type", defaultInstanceType) logger.Warningf(msg) } - - // Apply the coerced unknown values back into the config. - return cfg.Apply(ecfg.attrs) + // Construct a new config with the deprecated attributes removed. + for _, attr := range []string{"default-image-id", "default-instance-type"} { + delete(cfgAttrs, attr) + delete(ecfg.attrs, attr) + } + for k, v := range ecfg.attrs { + cfgAttrs[k] = v + } + return config.New(config.NoDefaults, cfgAttrs) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/config_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/config_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/config_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/config_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,13 +7,13 @@ "os" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -55,7 +55,6 @@ envVars map[string]string region string controlBucket string - toolsURL string useFloatingIP bool useDefaultSecurityGroup bool network string @@ -152,11 +151,6 @@ c.Assert(err, gc.IsNil) c.Assert(expected, gc.DeepEquals, actual) } - if t.toolsURL != "" { - toolsURL, ok := ecfg.ToolsURL() - c.Assert(ok, jc.IsTrue) - c.Assert(toolsURL, gc.Equals, t.toolsURL) - } if t.firewallMode != "" { c.Assert(ecfg.FirewallMode(), gc.Equals, t.firewallMode) } @@ -452,6 +446,28 @@ } } +func (s *ConfigSuite) TestDeprecatedAttributesRemoved(c *gc.C) { + s.setupEnvCredentials() + attrs := testing.FakeConfig().Merge(testing.Attrs{ + "type": "openstack", + "control-bucket": "x", + "default-image-id": "id-1234", + "default-instance-type": "big", + }) + + cfg, err := config.New(config.NoDefaults, attrs) + c.Assert(err, gc.IsNil) + // Keep err for validation below. + valid, err := providerInstance.Validate(cfg, nil) + c.Assert(err, gc.IsNil) + // Check deprecated attributes removed. + allAttrs := valid.AllAttrs() + for _, attr := range []string{"default-image-id", "default-instance-type"} { + _, ok := allAttrs[attr] + c.Assert(ok, jc.IsFalse) + } +} + func (s *ConfigSuite) TestPrepareInsertsUniqueControlBucket(c *gc.C) { s.setupEnvCredentials() attrs := testing.FakeConfig().Merge(testing.Attrs{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -153,6 +153,25 @@ } } }, + "com.ubuntu.cloud:server:12.04:ppc64": { + "release": "precise", + "version": "12.04", + "arch": "ppc64", + "versions": { + "20121111": { + "items": { + "inst33": { + "root_store": "ebs", + "virt": "pv", + "region": "some-region", + "id": "33" + } + }, + "pubname": "ubuntu-precise-12.04-ppc64-server-20121111", + "label": "release" + } + } + }, "com.ubuntu.cloud:server:12.10:amd64": { "release": "quantal", "version": "12.10", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/image.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/image.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/image.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/image.go 2014-03-20 12:52:38.000000000 +0000 @@ -43,7 +43,7 @@ return nil, err } // TODO (wallyworld): use an env parameter (default true) to mandate use of only signed image metadata. - matchingImages, err := imagemetadata.Fetch(sources, simplestreams.DefaultIndexPath, imageConstraint, false) + matchingImages, _, err := imagemetadata.Fetch(sources, simplestreams.DefaultIndexPath, imageConstraint, false) if err != nil { return nil, err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/local_test.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/local_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/local_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/local_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,6 +12,7 @@ "net/url" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goose/client" "launchpad.net/goose/identity" @@ -34,7 +35,6 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/openstack" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/version" ) @@ -587,6 +587,13 @@ c.Assert(err, gc.IsNil) } +func (s *localServerSuite) TestSupportedArchitectures(c *gc.C) { + env := s.Open(c) + a, err := env.SupportedArchitectures() + c.Assert(err, gc.IsNil) + c.Assert(a, gc.DeepEquals, []string{"amd64", "ppc64"}) +} + func (s *localServerSuite) TestFindImageBadDefaultImage(c *gc.C) { // Prevent falling over to the public datasource. s.PatchValue(&imagemetadata.DefaultBaseURL, "") @@ -605,7 +612,7 @@ params.Sources, err = imagemetadata.GetMetadataSources(env) c.Assert(err, gc.IsNil) params.Series = "raring" - image_ids, err := imagemetadata.ValidateImageMetadata(params) + image_ids, _, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.IsNil) c.Assert(image_ids, gc.DeepEquals, []string{"id-y"}) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/provider.go juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/provider.go --- juju-core-1.17.4/src/launchpad.net/juju-core/provider/openstack/provider.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/provider.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/goose/client" gooseerrors "launchpad.net/goose/errors" "launchpad.net/goose/identity" @@ -24,7 +24,6 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/instances" @@ -32,6 +31,7 @@ "launchpad.net/juju-core/environs/storage" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/names" "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" @@ -67,59 +67,75 @@ # https://juju.ubuntu.com/docs/config-openstack.html openstack: type: openstack - # use-floating-ip specifies whether a floating IP address is required - # to give the nodes a public IP address. Some installations assign public IP - # addresses by default without requiring a floating IP address. + + # use-floating-ip specifies whether a floating IP address is + # required to give the nodes a public IP address. Some + # installations assign public IP addresses by default without + # requiring a floating IP address. + # # use-floating-ip: false - # use-default-secgroup specifies whether new machine instances should have the "default" - # Openstack security group assigned. + # use-default-secgroup specifies whether new machine instances + # should have the "default" Openstack security group assigned. + # # use-default-secgroup: false - # network specifies the network label or uuid to bring machines up on, in - # the case where multiple networks exist. It may be omitted otherwise. + # network specifies the network label or uuid to bring machines up + # on, in the case where multiple networks exist. It may be omitted + # otherwise. + # # network: - # tools-metadata-url specifies the location of the Juju tools and metadata. It defaults to the - # global public tools metadata location https://streams.canonical.com/tools. - # tools-metadata-url: https://you-tools-metadata-url - - # image-metadata-url specifies the location of Ubuntu cloud image metadata. It defaults to the - # global public image metadata location https://cloud-images.ubuntu.com/releases. - # image-metadata-url: https://you-tools-metadata-url - - # image-stream chooses a simplestreams stream to select OS images from, - # for example daily or released images (or any other stream available on simplestreams). + # tools-metadata-url specifies the location of the Juju tools and + # metadata. It defaults to the global public tools metadata + # location https://streams.canonical.com/tools. + # + # tools-metadata-url: https://your-tools-metadata-url + + # image-metadata-url specifies the location of Ubuntu cloud image + # metadata. It defaults to the global public image metadata + # location https://cloud-images.ubuntu.com/releases. + # + # image-metadata-url: https://your-image-metadata-url + + # image-stream chooses a simplestreams stream to select OS images + # from, for example daily or released images (or any other stream + # available on simplestreams). + # # image-stream: "released" - # auth-url defaults to the value of the environment variable OS_AUTH_URL, - # but can be specified here. + # auth-url defaults to the value of the environment variable + # OS_AUTH_URL, but can be specified here. + # # auth-url: https://yourkeystoneurl:443/v2.0/ - # tenant-name holds the openstack tenant name. It defaults to - # the environment variable OS_TENANT_NAME. + # tenant-name holds the openstack tenant name. It defaults to the + # environment variable OS_TENANT_NAME. + # # tenant-name: - # region holds the openstack region. It defaults to - # the environment variable OS_REGION_NAME. + # region holds the openstack region. It defaults to the + # environment variable OS_REGION_NAME. + # # region: - # The auth-mode, username and password attributes - # are used for userpass authentication (the default). - + # The auth-mode, username and password attributes are used for + # userpass authentication (the default). + # # auth-mode holds the authentication mode. For user-password - # authentication, auth-mode should be "userpass" and username - # and password should be set appropriately; they default to - # the environment variables OS_USERNAME and OS_PASSWORD - # respectively. + # authentication, auth-mode should be "userpass" and username and + # password should be set appropriately; they default to the + # environment variables OS_USERNAME and OS_PASSWORD respectively. + # # auth-mode: userpass # username: # password: - - # For key-pair authentication, auth-mode should be "keypair" - # and access-key and secret-key should be set appropriately; they default to - # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY - # respectively. + + # For key-pair authentication, auth-mode should be "keypair" and + # access-key and secret-key should be set appropriately; they + # default to the environment variables OS_ACCESS_KEY and + # OS_SECRET_KEY respectively. + # # auth-mode: keypair # access-key: # secret-key: @@ -127,45 +143,59 @@ # https://juju.ubuntu.com/docs/config-hpcloud.html hpcloud: type: openstack - - # use-floating-ip specifies whether a floating IP address is required - # to give the nodes a public IP address. Some installations assign public IP - # addresses by default without requiring a floating IP address. + + # use-floating-ip specifies whether a floating IP address is + # required to give the nodes a public IP address. Some + # installations assign public IP addresses by default without + # requiring a floating IP address. + # # use-floating-ip: false - # use-default-secgroup specifies whether new machine instances should have the "default" - # Openstack security group assigned. + # use-default-secgroup specifies whether new machine instances + # should have the "default" Openstack security group assigned. + # # use-default-secgroup: false - # tenant-name holds the openstack tenant name. In HPCloud, this is - # synonymous with the project-name It defaults to - # the environment variable OS_TENANT_NAME. + # tenant-name holds the openstack tenant name. In HPCloud, this is + # synonymous with the project-name It defaults to the environment + # variable OS_TENANT_NAME. + # # tenant-name: - - # auth-url holds the keystone url for authentication. - # It defaults to the value of the environment variable OS_AUTH_URL. + + # image-stream chooses a simplestreams stream to select OS images + # from, for example daily or released images (or any other stream + # available on simplestreams). + # + # image-stream: "released" + + # auth-url holds the keystone url for authentication. It defaults + # to the value of the environment variable OS_AUTH_URL. + # # auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/ - # region holds the HP Cloud region (e.g. az-1.region-a.geo-1). - # It defaults to the environment variable OS_REGION_NAME. + # region holds the HP Cloud region (e.g. az-1.region-a.geo-1). It + # defaults to the environment variable OS_REGION_NAME. + # # region: - + # auth-mode holds the authentication mode. For user-password - # authentication, auth-mode should be "userpass" and username - # and password should be set appropriately; they default to - # the environment variables OS_USERNAME and OS_PASSWORD - # respectively. + # authentication, auth-mode should be "userpass" and username and + # password should be set appropriately; they default to the + # environment variables OS_USERNAME and OS_PASSWORD respectively. + # # auth-mode: userpass # username: # password: - - # For key-pair authentication, auth-mode should be "keypair" - # and access-key and secret-key should be set appropriately; they default to - # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY - # respectively. + + # For key-pair authentication, auth-mode should be "keypair" and + # access-key and secret-key should be set appropriately; they + # default to the environment variables OS_ACCESS_KEY and + # OS_SECRET_KEY respectively. + # # auth-mode: keypair # access-key: # secret-key: + `[1:] } @@ -204,7 +234,7 @@ } return &simplestreams.MetadataLookupParams{ Region: region, - Architectures: []string{"amd64", "arm"}, + Architectures: arch.AllSupportedArches, }, nil } @@ -278,6 +308,12 @@ type environ struct { name string + // archMutex gates access to supportedArchitectures + archMutex sync.Mutex + // supportedArchitectures caches the architectures + // for which images can be instantiated. + supportedArchitectures []string + ecfgMutex sync.Mutex imageBaseMutex sync.Mutex toolsBaseMutex sync.Mutex @@ -487,6 +523,26 @@ return e.name } +// SupportedArchitectures is specified on the EnvironCapability interface. +func (e *environ) SupportedArchitectures() ([]string, error) { + e.archMutex.Lock() + defer e.archMutex.Unlock() + if e.supportedArchitectures != nil { + return e.supportedArchitectures, nil + } + // Create a filter to get all images from our region and for the correct stream. + cloudSpec, err := e.Region() + if err != nil { + return nil, err + } + imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ + CloudSpec: cloudSpec, + Stream: e.Config().ImageStream(), + }) + e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) + return e.supportedArchitectures, err +} + func (e *environ) Storage() storage.Storage { e.ecfgMutex.Lock() stor := e.storageUnlocked @@ -585,7 +641,7 @@ } // Add the simplestreams source off the control bucket. e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource( - e.Storage(), storage.BaseImagesPath)) + "cloud storage", e.Storage(), storage.BaseImagesPath)) // Add the simplestreams base URL from keystone if it is defined. productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil) if err == nil { @@ -593,7 +649,7 @@ if !e.Config().SSLHostnameVerification() { verify = simplestreams.NoVerifySSLHostnames } - source := simplestreams.NewURLDataSource(productStreamsURL, verify) + source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify) e.imageSources = append(e.imageSources, source) } return e.imageSources, nil @@ -618,11 +674,12 @@ verify = simplestreams.NoVerifySSLHostnames } // Add the simplestreams source off the control bucket. - e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)) + e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource( + "cloud storage", e.Storage(), storage.BaseToolsPath)) // Add the simplestreams base URL from keystone if it is defined. toolsURL, err := e.client.MakeServiceURL("juju-tools", nil) if err == nil { - source := simplestreams.NewURLDataSource(toolsURL, verify) + source := simplestreams.NewURLDataSource("keystone catalog", toolsURL, verify) e.toolsSources = append(e.toolsSources, source) } return e.toolsSources, nil @@ -710,31 +767,30 @@ } // StartInstance is specified in the InstanceBroker interface. -func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (e *environ) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { - series := possibleTools.OneSeries() - arches := possibleTools.Arches() + series := args.Tools.OneSeries() + arches := args.Tools.Arches() spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ Region: e.ecfg().region(), Series: series, Arches: arches, - Constraints: cons, + Constraints: args.Constraints, }) if err != nil { return nil, nil, err } - tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch}) + tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) if err != nil { return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) } - machineConfig.Tools = tools[0] + args.MachineConfig.Tools = tools[0] - if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil { + if err := environs.FinishMachineConfig(args.MachineConfig, e.Config(), args.Constraints); err != nil { return nil, nil, err } - userData, err := environs.ComposeUserData(machineConfig) + userData, err := environs.ComposeUserData(args.MachineConfig) if err != nil { return nil, nil, fmt.Errorf("cannot make user data: %v", err) } @@ -760,7 +816,7 @@ } } cfg := e.Config() - groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) + groups, err := e.setUpGroups(args.MachineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) if err != nil { return nil, nil, fmt.Errorf("cannot set up groups: %v", err) } @@ -769,7 +825,7 @@ groupNames[i] = nova.SecurityGroupName{g.Name} } var opts = nova.RunServerOpts{ - Name: e.machineFullName(machineConfig.MachineId), + Name: e.machineFullName(args.MachineConfig.MachineId), FlavorId: spec.InstanceType.Id, ImageId: spec.Image.Id, UserData: userData, @@ -1192,18 +1248,26 @@ if region == "" { region = e.ecfg().region() } + cloudSpec, err := e.cloudSpec(region) + if err != nil { + return nil, err + } return &simplestreams.MetadataLookupParams{ Series: e.ecfg().DefaultSeries(), - Region: region, - Endpoint: e.ecfg().authURL(), - Architectures: []string{"amd64", "arm"}, + Region: cloudSpec.Region, + Endpoint: cloudSpec.Endpoint, + Architectures: arch.AllSupportedArches, }, nil } // Region is specified in the HasRegion interface. func (e *environ) Region() (simplestreams.CloudSpec, error) { + return e.cloudSpec(e.ecfg().region()) +} + +func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { return simplestreams.CloudSpec{ - Region: e.ecfg().region(), + Region: region, Endpoint: e.ecfg().authURL(), }, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/README juju-core-1.17.6/src/launchpad.net/juju-core/README --- juju-core-1.17.4/src/launchpad.net/juju-core/README 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/README 2014-03-20 12:52:38.000000000 +0000 @@ -121,3 +121,15 @@ bootstrap process. juju bootstrap -e your-environment --upload-tools {--debug} + +Installing bash completion for juju +=================================== + + make install-etc + +Will install Bash completion for `juju` cli to +`/etc/bash_completion.d/juju-core` It does dynamic completion for commands +requiring service, unit or machine names (like e.g. juju status , +juju ssh , juju terminate-machine , etc), +by parsing cached `juju status` output for speedup. It also does +command flags completion by parsing `juju help ...` output. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/replicaset/replicaset.go juju-core-1.17.6/src/launchpad.net/juju-core/replicaset/replicaset.go --- juju-core-1.17.4/src/launchpad.net/juju-core/replicaset/replicaset.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/replicaset/replicaset.go 2014-03-20 12:52:38.000000000 +0000 @@ -3,8 +3,10 @@ import ( "fmt" "io" + "strings" "time" + "github.com/juju/loggo" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" ) @@ -12,6 +14,8 @@ // MaxPeers defines the maximum number of peers that mongo supports. const MaxPeers = 7 +var logger = loggo.GetLogger("juju.replicaset") + // Initiate sets up a replica set with the given replica set name with the // single given member. It need be called only once for a given mongo replica // set. @@ -75,6 +79,45 @@ Votes *int `bson:"votes,omitempty"` } +func fmtConfigForLog(config *Config) string { + memberInfo := make([]string, len(config.Members)) + for i, member := range config.Members { + memberInfo[i] = fmt.Sprintf("Member{%d %q %v}", member.Id, member.Address, member.Tags) + + } + return fmt.Sprintf("{Name: %s, Version: %d, Members: {%s}}", + config.Name, config.Version, strings.Join(memberInfo, ", ")) +} + +// applyRelSetConfig applies the new config to the mongo session. It also logs +// what the changes are. It checks if the replica set changes cause the DB +// connection to be dropped. If so, it Refreshes the session and tries to Ping +// again. +func applyRelSetConfig(cmd string, session *mgo.Session, oldconfig, newconfig *Config) error { + logger.Debugf("%s() changing replica set\nfrom %s\n to %s", + cmd, fmtConfigForLog(oldconfig), fmtConfigForLog(newconfig)) + err := session.Run(bson.D{{"replSetReconfig", newconfig}}, nil) + // We will only try to Ping 2 times + for i := 0; i < 2; i++ { + if err == io.EOF { + // If the primary changes due to replSetReconfig, then all + // current connections are dropped. + // Refreshing should fix us up. + logger.Debugf("got EOF while running %s(), calling session.Refresh()", cmd) + session.Refresh() + } else if err != nil { + // For all errors that aren't EOF, return immediately + return err + } + // err is either nil or EOF and we called Refresh, so Ping to + // make sure we're actually connected + err = session.Ping() + // Change the command because it is the new command we ran + cmd = "Ping" + } + return err +} + // Add adds the given members to the session's replica set. Duplicates of // existing replicas will be ignored. // @@ -85,6 +128,7 @@ return err } + oldconfig := *config config.Version++ max := 0 for _, member := range config.Members { @@ -108,7 +152,7 @@ } config.Members = append(config.Members, newMember) } - return session.Run(bson.D{{"replSetReconfig", config}}, nil) + return applyRelSetConfig("Add", session, &oldconfig, config) } // Remove removes members with the given addresses from the replica set. It is @@ -118,6 +162,7 @@ if err != nil { return err } + oldconfig := *config config.Version++ for _, rem := range addrs { for n, repl := range config.Members { @@ -127,14 +172,7 @@ } } } - err = session.Run(bson.D{{"replSetReconfig", config}}, nil) - if err == io.EOF { - // EOF means we got disconnected due to the Remove... this is normal. - // Refreshing should fix us up. - session.Refresh() - err = nil - } - return err + return applyRelSetConfig("Remove", session, &oldconfig, config) } // Set changes the current set of replica set members. Members will have their @@ -145,6 +183,8 @@ return err } + // Copy the current configuration for logging + oldconfig := *config config.Version++ // Assign ids to members that did not previously exist, starting above the @@ -170,14 +210,7 @@ config.Members = members - err = session.Run(bson.D{{"replSetReconfig", config}}, nil) - if err == io.EOF { - // EOF means we got disconnected due to a Remove... this is normal. - // Refreshing should fix us up. - session.Refresh() - err = nil - } - return err + return applyRelSetConfig("Set", session, &oldconfig, config) } // Config reports information about the configuration of a given mongo node diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/replicaset/replicaset_test.go juju-core-1.17.6/src/launchpad.net/juju-core/replicaset/replicaset_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/replicaset/replicaset_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/replicaset/replicaset_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ gc "launchpad.net/gocheck" coretesting "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" ) @@ -52,11 +53,14 @@ return inst, err } -type MongoSuite struct{} +type MongoSuite struct { + testbase.LoggingSuite +} var _ = gc.Suite(&MongoSuite{}) func (s *MongoSuite) SetUpSuite(c *gc.C) { + s.LoggingSuite.SetUpSuite(c) var err error // do all this stuff here, since we don't want to have to redo it for each test root, err = newServer() @@ -69,6 +73,7 @@ } func (s *MongoSuite) TearDownTest(c *gc.C) { + s.LoggingSuite.TearDownTest(c) // remove all secondaries from the replicaset on test teardown session, err := root.DialDirect() if err != nil { @@ -138,6 +143,7 @@ } func (s *MongoSuite) TearDownSuite(c *gc.C) { + s.LoggingSuite.TearDownSuite(c) root.Destroy() } @@ -171,14 +177,21 @@ var err error - strategy := utils.AttemptStrategy{Total: time.Second * 30, Delay: time.Millisecond * 100} + // We use a delay of 31s. Our Mongo Dial timeout is 15s, so this gives + // us 2 attempts before we give up. + strategy := utils.AttemptStrategy{Total: time.Second * 31, Delay: time.Millisecond * 100} + start := time.Now() + attemptCount := 0 attempt := strategy.Start() for attempt.Next() { + attemptCount += 1 err = Add(session, members...) if err == nil || !attempt.HasNext() { break } + c.Logf("attempting to Add got error: %v", err) } + c.Logf("Add() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) expectedMembers := make([]Member, len(members)) @@ -189,14 +202,18 @@ } var cfg *Config + start = time.Now() + attemptCount = 0 attempt = strategy.Start() for attempt.Next() { + attemptCount += 1 cfg, err = CurrentConfig(session) if err == nil || !attempt.HasNext() { break } + c.Logf("attempting CurrentConfig got error: %v", err) } - + c.Logf("CurrentConfig() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) c.Assert(cfg.Name, gc.Equals, name) @@ -208,18 +225,34 @@ c.Assert(mems, gc.DeepEquals, expectedMembers) // Now remove the last two Members + start = time.Now() + attemptCount = 0 attempt = strategy.Start() for attempt.Next() { + attemptCount += 1 err = Remove(session, members[3].Address, members[4].Address) if err == nil || !attempt.HasNext() { break } + c.Logf("attempting Remove got error: %v", err) } + c.Logf("Remove() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) expectedMembers = expectedMembers[0:3] - mems, err = CurrentMembers(session) + start = time.Now() + attemptCount = 0 + attempt = strategy.Start() + for attempt.Next() { + attemptCount += 1 + mems, err = CurrentMembers(session) + if err == nil || !attempt.HasNext() { + break + } + c.Logf("attempting CurrentMembers got error: %v", err) + } + c.Logf("CurrentMembers() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) c.Assert(mems, gc.DeepEquals, expectedMembers) @@ -227,25 +260,34 @@ // plus the new arbiter mems = []Member{members[3], mems[2], mems[0], members[4]} + start = time.Now() + attemptCount = 0 attempt = strategy.Start() for attempt.Next() { + attemptCount += 1 err = Set(session, mems) if err == nil || !attempt.HasNext() { break } + c.Logf("attempting Set got error: %v", err) } - + c.Logf("Set() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) + start = time.Now() + attemptCount = 0 attempt = strategy.Start() for attempt.Next() { + attemptCount += 1 // can dial whichever replica address here, mongo will figure it out session = instances[0].MustDialDirect() err = session.Ping() if err == nil || !attempt.HasNext() { break } + c.Logf("attempting session.Ping() got error: %v after %s", err, time.Since(start)) } + c.Logf("session.Ping() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(err, gc.IsNil) expectedMembers = []Member{members[3], expectedMembers[2], expectedMembers[0], members[4]} @@ -254,14 +296,19 @@ expectedMembers[0].Id = 4 expectedMembers[3].Id = 5 + start = time.Now() + attemptCount = 0 attempt = strategy.Start() for attempt.Next() { + attemptCount += 1 mems, err = CurrentMembers(session) if err == nil || !attempt.HasNext() { break } + c.Logf("attempting CurrentMembers() got error: %v", err) } c.Assert(err, gc.IsNil) + c.Logf("CurrentMembers() %d attempts in %s", attemptCount, time.Since(start)) c.Assert(mems, gc.DeepEquals, expectedMembers) } @@ -305,7 +352,7 @@ defer inst2.Destroy() defer Remove(session, inst2.Addr()) - strategy := utils.AttemptStrategy{Total: time.Second * 30, Delay: time.Millisecond * 100} + strategy := utils.AttemptStrategy{Total: time.Second * 31, Delay: time.Millisecond * 100} attempt := strategy.Start() for attempt.Next() { err = Add(session, Member{Address: inst1.Addr()}, Member{Address: inst2.Addr()}) @@ -341,7 +388,7 @@ }}, } - strategy.Total = time.Second * 60 + strategy.Total = time.Second * 90 attempt = strategy.Start() var res *Status for attempt.Next() { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/rpc/jsoncodec/codec.go juju-core-1.17.6/src/launchpad.net/juju-core/rpc/jsoncodec/codec.go --- juju-core-1.17.4/src/launchpad.net/juju-core/rpc/jsoncodec/codec.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/rpc/jsoncodec/codec.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "sync" "sync/atomic" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/rpc" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/rpc/jsoncodec/codec_test.go juju-core-1.17.6/src/launchpad.net/juju-core/rpc/jsoncodec/codec_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/rpc/jsoncodec/codec_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/rpc/jsoncodec/codec_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "regexp" stdtesting "testing" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/rpc" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/rpc/reflect_test.go juju-core-1.17.6/src/launchpad.net/juju-core/rpc/reflect_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/rpc/reflect_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/rpc/reflect_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,10 +6,10 @@ import ( "reflect" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/rpc/rpcreflect" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/rpc/server.go juju-core-1.17.6/src/launchpad.net/juju-core/rpc/server.go --- juju-core-1.17.4/src/launchpad.net/juju-core/rpc/server.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/rpc/server.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,7 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/rpc/rpcreflect" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/scripts/release-public-tools/release-public-tools.sh juju-core-1.17.6/src/launchpad.net/juju-core/scripts/release-public-tools/release-public-tools.sh --- juju-core-1.17.4/src/launchpad.net/juju-core/scripts/release-public-tools/release-public-tools.sh 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/scripts/release-public-tools/release-public-tools.sh 2014-03-20 12:52:38.000000000 +0000 @@ -105,7 +105,7 @@ control_file=$1 arch=$(sed -n 's/^Architecture: \([a-z]\+\)/\1/p' $control_file) case "${arch}" in - "amd64" | "i386" | "armel" | "armhf" ) + "amd64" | "i386" | "armel" | "armhf" | "arm64" | "ppc64el" ) ;; *) echo "Invalid arch: $arch" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/scripts/win-installer/setup.iss juju-core-1.17.6/src/launchpad.net/juju-core/scripts/win-installer/setup.iss --- juju-core-1.17.4/src/launchpad.net/juju-core/scripts/win-installer/setup.iss 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/scripts/win-installer/setup.iss 2014-03-20 12:52:38.000000000 +0000 @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Juju" -#define MyAppVersion "1.17.4" +#define MyAppVersion "1.17.6" #define MyAppPublisher "Canonical, Ltd" #define MyAppURL "http://juju.ubuntu.com/" #define MyAppExeName "juju.exe" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/addmachine.go juju-core-1.17.6/src/launchpad.net/juju-core/state/addmachine.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/addmachine.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/addmachine.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "strconv" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/constraints" @@ -447,15 +448,15 @@ ops := []txn.Op{{ C: st.stateServers.Name, Id: environGlobalKey, - Assert: D{{ - "$and", []D{ - {{"machineids", D{{"$size", len(currentInfo.MachineIds)}}}}, - {{"votingmachineids", D{{"$size", len(currentInfo.VotingMachineIds)}}}}, + Assert: bson.D{{ + "$and", []bson.D{ + {{"machineids", bson.D{{"$size", len(currentInfo.MachineIds)}}}}, + {{"votingmachineids", bson.D{{"$size", len(currentInfo.VotingMachineIds)}}}}, }, }}, - Update: D{ - {"$addToSet", D{{"machineids", D{{"$each", newIds}}}}}, - {"$addToSet", D{{"votingmachineids", D{{"$each", newVotingIds}}}}}, + Update: bson.D{ + {"$addToSet", bson.D{{"machineids", bson.D{{"$each", newIds}}}}}, + {"$addToSet", bson.D{{"votingmachineids", bson.D{{"$each", newVotingIds}}}}}, }, }} return ops, nil diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/address.go juju-core-1.17.6/src/launchpad.net/juju-core/state/address.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/address.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/address.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,54 +6,11 @@ import ( "fmt" + "labix.org/v2/mgo/bson" + "labix.org/v2/mgo/txn" "launchpad.net/juju-core/instance" ) -// address represents the location of a machine, including metadata about what -// kind of location the address describes. -type address struct { - Value string - AddressType instance.AddressType - NetworkName string `bson:",omitempty"` - NetworkScope instance.NetworkScope `bson:",omitempty"` -} - -func newAddress(addr instance.Address) address { - stateaddr := address{ - Value: addr.Value, - AddressType: addr.Type, - NetworkName: addr.NetworkName, - NetworkScope: addr.NetworkScope, - } - return stateaddr -} - -func (addr *address) InstanceAddress() instance.Address { - instanceaddr := instance.Address{ - Value: addr.Value, - Type: addr.AddressType, - NetworkName: addr.NetworkName, - NetworkScope: addr.NetworkScope, - } - return instanceaddr -} - -func addressesToInstanceAddresses(addrs []address) []instance.Address { - instanceAddrs := make([]instance.Address, len(addrs)) - for i, addr := range addrs { - instanceAddrs[i] = addr.InstanceAddress() - } - return instanceAddrs -} - -func instanceAddressesToAddresses(instanceAddrs []instance.Address) []address { - addrs := make([]address, len(instanceAddrs)) - for i, addr := range instanceAddrs { - addrs[i] = newAddress(addr) - } - return addrs -} - // stateServerAddresses returns the list of internal addresses of the state // server machines. func (st *State) stateServerAddresses() ([]string, error) { @@ -62,7 +19,7 @@ } var allAddresses []addressMachine // TODO(rog) 2013/10/14 index machines on jobs. - err := st.machines.Find(D{{"jobs", JobManageEnviron}}).All(&allAddresses) + err := st.machines.Find(bson.D{{"jobs", JobManageEnviron}}).All(&allAddresses) if err != nil { return nil, err } @@ -105,9 +62,11 @@ return appendPort(addrs, config.StatePort()), nil } -// APIAddresses returns the list of cloud-internal addresses that +// APIAddressesFromMachines returns the list of cloud-internal addresses that // can be used to connect to the state API server. -func (st *State) APIAddresses() ([]string, error) { +// This method will be deprecated when API addresses are +// stored independently in their own document. +func (st *State) APIAddressesFromMachines() ([]string, error) { addrs, err := st.stateServerAddresses() if err != nil { return nil, err @@ -119,6 +78,45 @@ return appendPort(addrs, config.APIPort()), nil } +const apiHostPortsKey = "apiHostPorts" + +type apiHostPortsDoc struct { + APIHostPorts [][]hostPort +} + +// SetAPIHostPorts sets the addresses of the API server +// instances. Each server is represented by one element +// in the top level slice. +func (st *State) SetAPIHostPorts(hps [][]instance.HostPort) error { + doc := apiHostPortsDoc{ + APIHostPorts: instanceHostPortsToHostPorts(hps), + } + // We need to insert the document if it does not already + // exist to make this method work even on old environments + // where the document was not created by Initialize. + ops := []txn.Op{{ + C: st.stateServers.Name, + Id: apiHostPortsKey, + Update: bson.D{{"$set", bson.D{ + {"apihostports", doc.APIHostPorts}, + }}}, + }} + if err := st.runTransaction(ops); err != nil { + return fmt.Errorf("cannot set API addresses: %v", err) + } + return nil +} + +// APIHostPorts returns the API addresses as set by SetAPIHostPorts. +func (st *State) APIHostPorts() ([][]instance.HostPort, error) { + var doc apiHostPortsDoc + err := st.stateServers.Find(bson.D{{"_id", apiHostPortsKey}}).One(&doc) + if err != nil { + return nil, err + } + return hostPortsToInstanceHostPorts(doc.APIHostPorts), nil +} + type DeployerConnectionValues struct { StateAddresses []string APIAddresses []string @@ -140,3 +138,98 @@ APIAddresses: appendPort(addrs, config.APIPort()), }, nil } + +// address represents the location of a machine, including metadata about what +// kind of location the address describes. +type address struct { + Value string + AddressType instance.AddressType + NetworkName string `bson:",omitempty"` + NetworkScope instance.NetworkScope `bson:",omitempty"` +} + +type hostPort struct { + Value string + AddressType instance.AddressType + NetworkName string `bson:",omitempty"` + NetworkScope instance.NetworkScope `bson:",omitempty"` + Port int +} + +func newAddress(addr instance.Address) address { + return address{ + Value: addr.Value, + AddressType: addr.Type, + NetworkName: addr.NetworkName, + NetworkScope: addr.NetworkScope, + } +} + +func (addr *address) InstanceAddress() instance.Address { + return instance.Address{ + Value: addr.Value, + Type: addr.AddressType, + NetworkName: addr.NetworkName, + NetworkScope: addr.NetworkScope, + } +} + +func newHostPort(hp instance.HostPort) hostPort { + return hostPort{ + Value: hp.Value, + AddressType: hp.Type, + NetworkName: hp.NetworkName, + NetworkScope: hp.NetworkScope, + Port: hp.Port, + } +} + +func (hp *hostPort) InstanceHostPort() instance.HostPort { + return instance.HostPort{ + Address: instance.Address{ + Value: hp.Value, + Type: hp.AddressType, + NetworkName: hp.NetworkName, + NetworkScope: hp.NetworkScope, + }, + Port: hp.Port, + } +} + +func addressesToInstanceAddresses(addrs []address) []instance.Address { + instanceAddrs := make([]instance.Address, len(addrs)) + for i, addr := range addrs { + instanceAddrs[i] = addr.InstanceAddress() + } + return instanceAddrs +} + +func instanceAddressesToAddresses(instanceAddrs []instance.Address) []address { + addrs := make([]address, len(instanceAddrs)) + for i, addr := range instanceAddrs { + addrs[i] = newAddress(addr) + } + return addrs +} + +func hostPortsToInstanceHostPorts(insts [][]hostPort) [][]instance.HostPort { + instanceHostPorts := make([][]instance.HostPort, len(insts)) + for i, hps := range insts { + instanceHostPorts[i] = make([]instance.HostPort, len(hps)) + for j, hp := range hps { + instanceHostPorts[i][j] = hp.InstanceHostPort() + } + } + return instanceHostPorts +} + +func instanceHostPortsToHostPorts(instanceHostPorts [][]instance.HostPort) [][]hostPort { + hps := make([][]hostPort, len(instanceHostPorts)) + for i, instanceHps := range instanceHostPorts { + hps[i] = make([]hostPort, len(instanceHps)) + for j, hp := range instanceHps { + hps[i][j] = newHostPort(hp) + } + } + return hps +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/annotator.go juju-core-1.17.6/src/launchpad.net/juju-core/state/annotator.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/annotator.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/annotator.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "strings" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/utils" @@ -117,7 +118,7 @@ C: a.st.annotations.Name, Id: a.globalKey, Assert: txn.DocExists, - Update: D{{"$set", toUpdate}, {"$unset", toRemove}}, + Update: bson.D{{"$set", toUpdate}, {"$unset", toRemove}}, }} } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/machine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/machine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/machine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/machine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func TestAll(t *stdtesting.T) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/state.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/state.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/state.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/state.go 2014-03-20 12:52:38.000000000 +0000 @@ -85,8 +85,8 @@ // SetPassword sets the password associated with the agent's entity. func (m *Entity) SetPassword(password string) error { var results params.ErrorResults - args := params.PasswordChanges{ - Changes: []params.PasswordChange{{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{{ Tag: m.tag, Password: password, }}, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/unit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/unit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/agent/unit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/agent/unit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/apiclient.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/apiclient.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/apiclient.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/apiclient.go 2014-03-20 12:52:38.000000000 +0000 @@ -25,6 +25,7 @@ type State struct { client *rpc.Conn conn *websocket.Conn + addr string // authTag holds the authenticated entity's tag after login. authTag string @@ -127,6 +128,7 @@ st := &State{ client: client, conn: conn, + addr: cfg.Location.Host, serverRoot: "https://" + cfg.Location.Host, tag: info.Tag, password: info.Password, @@ -186,3 +188,8 @@ func (s *State) RPCClient() *rpc.Conn { return s.client } + +// Addr returns the address used to connect to the RPC server. +func (s *State) Addr() string { + return s.addr +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/client.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/client.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,12 +9,14 @@ "io/ioutil" "net/http" "os" + "strings" "time" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -141,6 +143,15 @@ return results.PublicAddress, err } +// PrivateAddress returns the private address of the specified +// machine or unit. +func (c *Client) PrivateAddress(target string) (string, error) { + var results params.PrivateAddressResults + p := params.PrivateAddress{Target: target} + err := c.st.Call("Client", "", "PrivateAddress", p, &results) + return results.PrivateAddress, err +} + // ServiceSetYAML sets configuration options on a service // given options in YAML format. func (c *Client) ServiceSetYAML(service string, yaml string) error { @@ -419,6 +430,20 @@ return c.st.Call("Client", "", "SetEnvironAgentVersion", args, nil) } +// FindTools returns a List containing all tools matching the specified parameters. +func (c *Client) FindTools(majorVersion, minorVersion int, + series, arch string) (result params.FindToolsResults, err error) { + + args := params.FindToolsParams{ + MajorVersion: majorVersion, + MinorVersion: minorVersion, + Arch: arch, + Series: series, + } + err = c.st.Call("Client", "", "FindTools", args, &result) + return result, err +} + // RunOnAllMachines runs the command on all the machines with the specified // timeout. func (c *Client) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { @@ -534,3 +559,62 @@ args := params.CharmURL{URL: curl.String()} return c.st.Call("Client", "", "AddCharm", args, nil) } + +func (c *Client) UploadTools( + toolsFilename string, vers version.Binary, fakeSeries ...string, +) ( + tools *tools.Tools, err error, +) { + toolsTarball, err := os.Open(toolsFilename) + if err != nil { + return nil, err + } + defer toolsTarball.Close() + + // Prepare the upload request. + url := fmt.Sprintf("%s/tools?binaryVersion=%s&series=%s", c.st.serverRoot, vers, strings.Join(fakeSeries, ",")) + req, err := http.NewRequest("POST", url, toolsTarball) + if err != nil { + return nil, fmt.Errorf("cannot create upload request: %v", err) + } + req.SetBasicAuth(c.st.tag, c.st.password) + req.Header.Set("Content-Type", "application/x-tar-gz") + + // Send the request. + + // BUG(dimitern) 2013-12-17 bug #1261780 + // Due to issues with go 1.1.2, fixed later, we cannot use a + // regular TLS client with the CACert here, because we get "x509: + // cannot validate certificate for 127.0.0.1 because it doesn't + // contain any IP SANs". Once we use a later go version, this + // should be changed to connect to the API server with a regular + // HTTP+TLS enabled client, using the CACert (possily cached, like + // the tag and password) passed in api.Open()'s info argument. + resp, err := utils.GetNonValidatingHTTPClient().Do(req) + if err != nil { + return nil, fmt.Errorf("cannot upload charm: %v", err) + } + if resp.StatusCode == http.StatusMethodNotAllowed { + // API server is older than 1.17.5, so tools upload + // is not supported; notify the client. + return nil, ¶ms.Error{ + Message: "tools upload is not supported by the API server", + Code: params.CodeNotImplemented, + } + } + + // Now parse the response & return. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read tools upload response: %v", err) + } + defer resp.Body.Close() + var jsonResponse params.ToolsResult + if err := json.Unmarshal(body, &jsonResponse); err != nil { + return nil, fmt.Errorf("cannot unmarshal upload response: %v", err) + } + if err := jsonResponse.Error; err != nil { + return nil, fmt.Errorf("error uploading tools: %v", err) + } + return jsonResponse.Tools, nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/client_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/client_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "net/http" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -14,7 +15,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type clientSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package testing import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -11,7 +12,6 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/watcher" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) const ( @@ -76,17 +76,26 @@ // Initial event. wc.AssertOneChange() - // Change the environment configuration, check it's detected. - attrs := envConfig.AllAttrs() - attrs["type"] = "blah" - newConfig, err := config.New(config.NoDefaults, attrs) + // Change the environment configuration by updating an existing attribute, check it's detected. + newAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} + err = s.st.UpdateEnvironConfig(newAttrs, nil, nil) c.Assert(err, gc.IsNil) - err = s.st.SetEnvironConfig(newConfig, envConfig) + wc.AssertOneChange() + + // Change the environment configuration by adding a new attribute, check it's detected. + newAttrs = map[string]interface{}{"foo": "bar"} + err = s.st.UpdateEnvironConfig(newAttrs, nil, nil) + c.Assert(err, gc.IsNil) + wc.AssertOneChange() + + // Change the environment configuration by removing an attribute, check it's detected. + err = s.st.UpdateEnvironConfig(map[string]interface{}{}, []string{"foo"}, nil) c.Assert(err, gc.IsNil) wc.AssertOneChange() // Change it back to the original config. - err = s.st.SetEnvironConfig(envConfig, newConfig) + oldAttrs := map[string]interface{}{"logging-config": envConfig.AllAttrs()["logging-config"]} + err = s.st.UpdateEnvironConfig(oldAttrs, nil, nil) c.Assert(err, gc.IsNil) wc.AssertOneChange() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func TestAll(t *stdtesting.T) { @@ -254,7 +254,7 @@ err := s.machine.SetAddresses(addrs) c.Assert(err, gc.IsNil) - stateAddresses, err := s.State.APIAddresses() + stateAddresses, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) c.Assert(len(stateAddresses), gc.Equals, 1) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/deployer/unit.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/unit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/deployer/unit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/unit.go 2014-03-20 12:52:38.000000000 +0000 @@ -61,8 +61,8 @@ // SetPassword sets the unit's password. func (u *Unit) SetPassword(password string) error { var result params.ErrorResults - args := params.PasswordChanges{ - Changes: []params.PasswordChange{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: u.tag, Password: password}, }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/machine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/machine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/machine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/machine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package firewaller_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" @@ -11,7 +12,6 @@ "launchpad.net/juju-core/state/api/firewaller" "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type machineSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/service_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/service_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/service_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/service_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,12 +4,12 @@ package firewaller_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state/api/firewaller" "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type serviceSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/state_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/state_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/state_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/state_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,13 +4,12 @@ package firewaller_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type stateSuite struct { @@ -81,16 +80,14 @@ wc.AssertOneChange() // Change the environment configuration, check it's detected. - attrs := envConfig.AllAttrs() - attrs["type"] = "blah" - newConfig, err := config.New(config.NoDefaults, attrs) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(newConfig, envConfig) + newAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} + err = s.State.UpdateEnvironConfig(newAttrs, nil, nil) c.Assert(err, gc.IsNil) wc.AssertOneChange() // Change it back to the original config. - err = s.State.SetEnvironConfig(envConfig, newConfig) + oldAttrs := map[string]interface{}{"logging-config": envConfig.AllAttrs()["logging-config"]} + err = s.State.UpdateEnvironConfig(oldAttrs, nil, nil) c.Assert(err, gc.IsNil) wc.AssertOneChange() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/unit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/unit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/firewaller/unit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/unit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,13 +4,13 @@ package firewaller_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state/api/firewaller" "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type unitSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/keymanager/client_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keymanager/client_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/keymanager/client_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keymanager/client_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" keymanagerserver "launchpad.net/juju-core/state/apiserver/keymanager" keymanagertesting "launchpad.net/juju-core/state/apiserver/keymanager/testing" - "launchpad.net/juju-core/state/testing" "launchpad.net/juju-core/utils/ssh" sshtesting "launchpad.net/juju-core/utils/ssh/testing" ) @@ -35,7 +34,7 @@ } func (s *keymanagerSuite) setAuthorisedKeys(c *gc.C, keys string) { - err := testing.UpdateConfig(s.BackingState, map[string]interface{}{"authorized-keys": keys}) + err := s.BackingState.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keys}, nil, nil) c.Assert(err, gc.IsNil) } @@ -44,7 +43,7 @@ key2 := sshtesting.ValidKeyTwo.Key s.setAuthorisedKeys(c, strings.Join([]string{key1, key2}, "\n")) - keyResults, err := s.keymanager.ListKeys(ssh.Fingerprints, "admin") + keyResults, err := s.keymanager.ListKeys(ssh.Fingerprints, state.AdminUser) c.Assert(err, gc.IsNil) c.Assert(len(keyResults), gc.Equals, 1) result := keyResults[0] @@ -80,7 +79,7 @@ s.setAuthorisedKeys(c, key1) newKeys := []string{sshtesting.ValidKeyTwo.Key, sshtesting.ValidKeyThree.Key, "invalid"} - errResults, err := s.keymanager.AddKeys("admin", newKeys...) + errResults, err := s.keymanager.AddKeys(state.AdminUser, newKeys...) c.Assert(err, gc.IsNil) c.Assert(errResults, gc.DeepEquals, []params.ErrorResult{ {Error: nil}, @@ -124,7 +123,7 @@ initialKeys := []string{key1, key2, key3, "invalid"} s.setAuthorisedKeys(c, strings.Join(initialKeys, "\n")) - errResults, err := s.keymanager.DeleteKeys("admin", sshtesting.ValidKeyTwo.Fingerprint, "user@host", "missing") + errResults, err := s.keymanager.DeleteKeys(state.AdminUser, sshtesting.ValidKeyTwo.Fingerprint, "user@host", "missing") c.Assert(err, gc.IsNil) c.Assert(errResults, gc.DeepEquals, []params.ErrorResult{ {Error: nil}, @@ -141,7 +140,7 @@ s.setAuthorisedKeys(c, key1) keyIds := []string{"lp:validuser", "invalid-key"} - errResults, err := s.keymanager.ImportKeys("admin", keyIds...) + errResults, err := s.keymanager.ImportKeys(state.AdminUser, keyIds...) c.Assert(err, gc.IsNil) c.Assert(errResults, gc.DeepEquals, []params.ErrorResult{ {Error: nil}, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -54,7 +54,7 @@ } func (s *keyupdaterSuite) setAuthorisedKeys(c *gc.C, keys string) { - err := testing.UpdateConfig(s.BackingState, map[string]interface{}{"authorized-keys": keys}) + err := s.BackingState.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keys}, nil, nil) c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/logger/logger_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/logger/logger_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/logger/logger_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/logger/logger_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -50,7 +50,7 @@ } func (s *loggerSuite) setLoggingConfig(c *gc.C, loggingConfig string) { - err := testing.UpdateConfig(s.BackingState, map[string]interface{}{"logging-config": loggingConfig}) + err := s.BackingState.UpdateEnvironConfig(map[string]interface{}{"logging-config": loggingConfig}, nil, nil) c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func TestAll(t *stdtesting.T) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/params/internal.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/internal.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/params/internal.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/internal.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,16 +13,6 @@ "launchpad.net/juju-core/version" ) -// Entity identifies a single entity. -type Entity struct { - Tag string -} - -// Entities identifies multiple entities. -type Entities struct { - Entities []Entity -} - // MachineContainersParams holds the arguments for making a SetSupportedContainers // API call. type MachineContainersParams struct { @@ -414,6 +404,20 @@ Results []ToolsResult } +// FindToolsParams defines parameters for the FindTools method. +type FindToolsParams struct { + MajorVersion int + MinorVersion int + Arch string + Series string +} + +// FindToolsResults holds a list of tools from FindTools and any error. +type FindToolsResults struct { + List tools.List + Error *Error +} + // Version holds a specific binary version. type Version struct { Version version.Binary @@ -433,18 +437,6 @@ AgentTools []EntityVersion } -// PasswordChanges holds the parameters for making a SetPasswords call. -type PasswordChanges struct { - Changes []PasswordChange -} - -// PasswordChange specifies a password change for the entity -// with the given tag. -type PasswordChange struct { - Tag string - Password string -} - // NotifyWatchResult holds a NotifyWatcher id and an error (if any). type NotifyWatchResult struct { NotifyWatcherId string diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/params/params.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/params/params.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,6 +16,28 @@ "launchpad.net/juju-core/version" ) +// Entity identifies a single entity. +type Entity struct { + Tag string +} + +// Entities identifies multiple entities. +type Entities struct { + Entities []Entity +} + +// EntityPasswords holds the parameters for making a SetPasswords call. +type EntityPasswords struct { + Changes []EntityPassword +} + +// EntityPassword specifies a password change for the entity +// with the given tag. +type EntityPassword struct { + Tag string + Password string +} + // ErrorResults holds the results of calling a bulk operation which // returns no data, only an error result. The order and // number of elements matches the operations specified in the request. @@ -214,6 +236,16 @@ PublicAddress string } +// PrivateAddress holds parameters for the PrivateAddress call. +type PrivateAddress struct { + Target string +} + +// PrivateAddressResults holds results of the PrivateAddress call. +type PrivateAddressResults struct { + PrivateAddress string +} + // Resolved holds parameters for the Resolved call. type Resolved struct { UnitName string diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/provisioner/machine.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/machine.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/provisioner/machine.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/machine.go 2014-03-20 12:52:38.000000000 +0000 @@ -206,8 +206,8 @@ // SetPassword sets the machine's password. func (m *Machine) SetPassword(password string) error { var result params.ErrorResults - args := params.PasswordChanges{ - Changes: []params.PasswordChange{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: m.tag, Password: password}, }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -19,7 +20,6 @@ "launchpad.net/juju-core/state/api/provisioner" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -344,7 +344,7 @@ }) c.Assert(err, gc.IsNil) - apiAddresses, err := s.State.APIAddresses() + apiAddresses, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) addresses, err := s.provisioner.APIAddresses() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/charm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/charm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,11 @@ package uniter_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/state/api/uniter" - jc "launchpad.net/juju-core/testing/checkers" ) type charmSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/relation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/relation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,12 +4,12 @@ package uniter_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/uniter" - jc "launchpad.net/juju-core/testing/checkers" ) type relationSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/relationunit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relationunit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/relationunit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relationunit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package uniter_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -11,7 +12,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/uniter" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) // commonRelationSuiteMixin contains fields used by both relationSuite diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/service_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/service_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/service_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/service_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,12 +4,12 @@ package uniter_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/uniter" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type serviceSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/state_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/state_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/state_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/state_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,7 +14,7 @@ var _ = gc.Suite(&stateSuite{}) func (s *stateSuite) TestAPIAddresses(c *gc.C) { - stateAPIAddresses, err := s.State.APIAddresses() + stateAPIAddresses, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) addresses, err := s.uniter.APIAddresses() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/unit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/unit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/uniter/unit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/unit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package uniter_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/uniter" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type unitSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/upgrader/unitupgrader_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/unitupgrader_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/upgrader/unitupgrader_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/unitupgrader_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,7 @@ package upgrader_test import ( - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" @@ -18,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/upgrader" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" @@ -34,9 +29,6 @@ rawMachine *state.Machine rawUnit *state.Unit - // Tools for the assigned machine. - fakeTools *tools.Tools - st *upgrader.State } @@ -52,21 +44,6 @@ c.Assert(err, gc.IsNil) s.stateAPI = s.OpenAPIAs(c, s.rawUnit.Tag(), password) - // Set up fake downloaded tools for the assigned machine. - fakeToolsPath := filepath.Join(s.DataDir(), "tools", version.Current.String()) - err = os.MkdirAll(fakeToolsPath, 0700) - c.Assert(err, gc.IsNil) - s.fakeTools = &tools.Tools{ - Version: version.Current, - URL: "fake-url", - Size: 1234, - SHA256: "checksum", - } - toolsMetadataData, err := json.Marshal(s.fakeTools) - c.Assert(err, gc.IsNil) - err = ioutil.WriteFile(filepath.Join(fakeToolsPath, "downloaded-tools.txt"), []byte(toolsMetadataData), 0644) - c.Assert(err, gc.IsNil) - // Create the upgrader facade. s.st = s.stateAPI.Upgrader() c.Assert(s.st, gc.NotNil) @@ -132,7 +109,8 @@ // running set. We want to be upgraded to cur.Version stateTools, _, err := s.st.Tools(s.rawUnit.Tag()) c.Assert(err, gc.IsNil) - c.Assert(stateTools, gc.DeepEquals, s.fakeTools) + c.Check(stateTools.Version.Number, gc.DeepEquals, version.Current.Number) + c.Assert(stateTools.URL, gc.NotNil) } func (s *unitUpgraderSuite) TestWatchAPIVersion(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" envtesting "launchpad.net/juju-core/environs/testing" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/state/api/upgrader" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/client.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/client.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,45 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager + +import ( + "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" +) + +// TODO(mattyw) 2014-03-07 bug #1288750 +// Need a SetPassword method. +type Client struct { + st *api.State +} + +func NewClient(st *api.State) *Client { + return &Client{st} +} + +func (c *Client) Close() error { + return c.st.Close() +} + +func (c *Client) AddUser(tag, password string) error { + u := params.EntityPassword{Tag: tag, Password: password} + p := params.EntityPasswords{Changes: []params.EntityPassword{u}} + results := new(params.ErrorResults) + err := c.st.Call("UserManager", "", "AddUser", p, results) + if err != nil { + return err + } + return results.OneError() +} + +func (c *Client) RemoveUser(tag string) error { + u := params.Entity{Tag: tag} + p := params.Entities{Entities: []params.Entity{u}} + results := new(params.ErrorResults) + err := c.st.Call("UserManager", "", "RemoveUser", p, results) + if err != nil { + return err + } + return results.OneError() +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/client_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/client_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,58 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager_test + +import ( + gc "launchpad.net/gocheck" + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/usermanager" +) + +type usermanagerSuite struct { + jujutesting.JujuConnSuite + + usermanager *usermanager.Client +} + +var _ = gc.Suite(&usermanagerSuite{}) + +func (s *usermanagerSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + s.usermanager = usermanager.NewClient(s.APIState) + c.Assert(s.usermanager, gc.NotNil) +} + +func (s *usermanagerSuite) TestAddUser(c *gc.C) { + err := s.usermanager.AddUser("foobar", "password") + c.Assert(err, gc.IsNil) + _, err = s.State.User("foobar") + c.Assert(err, gc.IsNil) +} + +func (s *usermanagerSuite) TestRemoveUser(c *gc.C) { + err := s.usermanager.AddUser("foobar", "password") + c.Assert(err, gc.IsNil) + _, err = s.State.User("foobar") + c.Assert(err, gc.IsNil) + + err = s.usermanager.RemoveUser("foobar") + c.Assert(err, gc.IsNil) + user, err := s.State.User("foobar") + c.Assert(user.IsDeactivated(), gc.Equals, true) +} + +func (s *usermanagerSuite) TestAddExistingUser(c *gc.C) { + err := s.usermanager.AddUser("foobar", "password") + c.Assert(err, gc.IsNil) + + // Try adding again + err = s.usermanager.AddUser("foobar", "password") + c.Assert(err, gc.ErrorMatches, "Failed to create user: user already exists") +} + +func (s *usermanagerSuite) TestCantRemoveAdminUser(c *gc.C) { + err := s.usermanager.RemoveUser(state.AdminUser) + c.Assert(err, gc.ErrorMatches, "Failed to remove user: Can't deactivate admin user") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/api/usermanager/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager_test + +import ( + stdtesting "testing" + + "launchpad.net/juju-core/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/agent/agent_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/agent/agent_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/agent/agent_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/agent/agent_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -156,8 +156,8 @@ } func (s *agentSuite) TestSetPasswords(c *gc.C) { - results, err := s.agent.SetPasswords(params.PasswordChanges{ - Changes: []params.PasswordChange{ + results, err := s.agent.SetPasswords(params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: "machine-0", Password: "xxx-12345678901234567890"}, {Tag: "machine-1", Password: "yyy-12345678901234567890"}, {Tag: "machine-42", Password: "zzz-12345678901234567890"}, @@ -178,8 +178,8 @@ } func (s *agentSuite) TestShortSetPasswords(c *gc.C) { - results, err := s.agent.SetPasswords(params.PasswordChanges{ - Changes: []params.PasswordChange{ + results, err := s.agent.SetPasswords(params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: "machine-1", Password: "yyy"}, }, }) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/apiserver.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/apiserver.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/apiserver.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/apiserver.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,7 +12,7 @@ "time" "code.google.com/p/go.net/websocket" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/rpc" @@ -152,7 +152,14 @@ }() mux := http.NewServeMux() mux.HandleFunc("/", srv.apiHandler) - mux.Handle("/charms", &charmsHandler{state: srv.state, dataDir: srv.dataDir}) + charmHandler := &charmsHandler{httpHandler: httpHandler{state: srv.state}, dataDir: srv.dataDir} + // charmHandler itself provides the errorSender implementation for the embedded httpHandler. + charmHandler.httpHandler.errorSender = charmHandler + mux.Handle("/charms", charmHandler) + toolsHandler := &toolsHandler{httpHandler{state: srv.state}} + // toolsHandler itself provides the errorSender implementation for the embedded httpHandler. + toolsHandler.httpHandler.errorSender = toolsHandler + mux.Handle("/tools", toolsHandler) // The error from http.Serve is not interesting. http.Serve(lis, mux) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,7 @@ package charmrevisionupdater import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/log" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/updater_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package charmrevisionupdater_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing" "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type charmVersionSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charms.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charms.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,6 @@ import ( "archive/zip" "crypto/sha256" - "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -16,29 +15,29 @@ "net/http" "net/url" "os" + "path" "path/filepath" + "sort" "strconv" "strings" "github.com/errgo/errgo" "launchpad.net/juju-core/charm" - envtesting "launchpad.net/juju-core/environs/testing" - "launchpad.net/juju-core/names" - "launchpad.net/juju-core/state" + "launchpad.net/juju-core/environs" "launchpad.net/juju-core/state/api/params" - "launchpad.net/juju-core/state/apiserver/common" + ziputil "launchpad.net/juju-core/utils/zip" ) // charmsHandler handles charm upload through HTTPS in the API server. type charmsHandler struct { - state *state.State + httpHandler dataDir string } -// zipContentsSenderFunc functions are responsible of sending a zip archive -// related response. The zip archive can be accessed through the given reader. -type zipContentsSenderFunc func(w http.ResponseWriter, r *http.Request, reader *zip.ReadCloser) +// bundleContentSenderFunc functions are responsible for sending a +// response related to a charm bundle. +type bundleContentSenderFunc func(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := h.authenticate(r); err != nil { @@ -65,10 +64,10 @@ h.sendError(w, http.StatusBadRequest, err.Error()) } else if filePath == "" { // The client requested the list of charm files. - sendZipContents(w, r, charmArchivePath, h.fileListSender) + sendBundleContent(w, r, charmArchivePath, h.manifestSender) } else { // The client requested a specific file. - sendZipContents(w, r, charmArchivePath, h.fileSender(filePath)) + sendBundleContent(w, r, charmArchivePath, h.fileSender(filePath)) } default: h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsupported method: %q", r.Method)) @@ -87,43 +86,52 @@ return nil } -// sendZipContents uses the given zipContentsSenderFunc to send a response -// related to the zip archive located in the given archivePath. -func sendZipContents(w http.ResponseWriter, r *http.Request, archivePath string, zipContentsSender zipContentsSenderFunc) { - reader, err := zip.OpenReader(archivePath) +// sendBundleContent uses the given bundleContentSenderFunc to send a response +// related to the charm archive located in the given archivePath. +func sendBundleContent(w http.ResponseWriter, r *http.Request, archivePath string, sender bundleContentSenderFunc) { + bundle, err := charm.ReadBundle(archivePath) if err != nil { http.Error( w, fmt.Sprintf("unable to read archive in %q: %v", archivePath, err), http.StatusInternalServerError) return } - defer reader.Close() - // The zipContentsSenderFunc will handle the zip contents, set up and send - // an appropriate response. - zipContentsSender(w, r, reader) + // The bundleContentSenderFunc will set up and send an appropriate response. + sender(w, r, bundle) } -// fileListSender sends a JSON-encoded response to the client including the -// list of files contained in the zip archive. -func (h *charmsHandler) fileListSender(w http.ResponseWriter, r *http.Request, reader *zip.ReadCloser) { - var files []string - for _, file := range reader.File { - fileInfo := file.FileInfo() - if !fileInfo.IsDir() { - files = append(files, file.Name) - } +// manifestSender sends a JSON-encoded response to the client including the +// list of files contained in the charm bundle. +func (h *charmsHandler) manifestSender(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) { + manifest, err := bundle.Manifest() + if err != nil { + http.Error( + w, fmt.Sprintf("unable to read archive in %q: %v", bundle.Path, err), + http.StatusInternalServerError) + return } - h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{Files: files}) + h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{Files: manifest.SortedValues()}) } -// fileSender returns a zipContentsSenderFunc which is responsible of sending -// the contents of filePath included in the given zip. -// A 404 page not found is returned if path does not exist in the zip. -// A 403 forbidden error is returned if path points to a directory. -func (h *charmsHandler) fileSender(filePath string) zipContentsSenderFunc { - return func(w http.ResponseWriter, r *http.Request, reader *zip.ReadCloser) { - for _, file := range reader.File { - if h.fixPath(file.Name) != filePath { +// fileSender returns a bundleContentSenderFunc which is responsible for sending +// the contents of filePath included in the given charm bundle. If filePath does +// not identify a file or a symlink, a 403 forbidden error is returned. +func (h *charmsHandler) fileSender(filePath string) bundleContentSenderFunc { + return func(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) { + // TODO(fwereade) 2014-01-27 bug #1285685 + // This doesn't handle symlinks helpfully, and should be talking in + // terms of bundles rather than zip readers; but this demands thought + // and design and is not amenable to a quick fix. + zipReader, err := zip.OpenReader(bundle.Path) + if err != nil { + http.Error( + w, fmt.Sprintf("unable to read charm: %v", err), + http.StatusInternalServerError) + return + } + defer zipReader.Close() + for _, file := range zipReader.File { + if path.Clean(file.Name) != filePath { continue } fileInfo := file.FileInfo() @@ -131,21 +139,21 @@ http.Error(w, "directory listing not allowed", http.StatusForbidden) return } - if contents, err := file.Open(); err != nil { + contents, err := file.Open() + if err != nil { http.Error( w, fmt.Sprintf("unable to read file %q: %v", filePath, err), http.StatusInternalServerError) return - } else { - defer contents.Close() - ctype := mime.TypeByExtension(filepath.Ext(filePath)) - if ctype != "" { - w.Header().Set("Content-Type", ctype) - } - w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) - w.WriteHeader(http.StatusOK) - io.Copy(w, contents) } + defer contents.Close() + ctype := mime.TypeByExtension(filepath.Ext(filePath)) + if ctype != "" { + w.Header().Set("Content-Type", ctype) + } + w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) + w.WriteHeader(http.StatusOK) + io.Copy(w, contents) return } http.NotFound(w, r) @@ -158,45 +166,6 @@ return h.sendJSON(w, statusCode, ¶ms.CharmsResponse{Error: message}) } -// authenticate parses HTTP basic authentication and authorizes the -// request by looking up the provided tag and password against state. -func (h *charmsHandler) authenticate(r *http.Request) error { - parts := strings.Fields(r.Header.Get("Authorization")) - if len(parts) != 2 || parts[0] != "Basic" { - // Invalid header format or no header provided. - return fmt.Errorf("invalid request format") - } - // Challenge is a base64-encoded "tag:pass" string. - // See RFC 2617, Section 2. - challenge, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { - return fmt.Errorf("invalid request format") - } - tagPass := strings.SplitN(string(challenge), ":", 2) - if len(tagPass) != 2 { - return fmt.Errorf("invalid request format") - } - entity, err := checkCreds(h.state, params.Creds{ - AuthTag: tagPass[0], - Password: tagPass[1], - }) - if err != nil { - return err - } - // Only allow users, not agents. - _, _, err = names.ParseTag(entity.Tag(), names.UserTagKind) - if err != nil { - return common.ErrBadCreds - } - return err -} - -// authError sends an unauthorized error. -func (h *charmsHandler) authError(w http.ResponseWriter) { - w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) - h.sendError(w, http.StatusUnauthorized, "unauthorized") -} - // processPost handles a charm upload POST request after authentication. func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { query := r.URL.Query() @@ -273,25 +242,26 @@ if err != nil { return errgo.Annotate(err, "cannot read charm archive") } - if rootDir == "" { + if rootDir == "." { // Normal charm, just use charm.ReadBundle(). return nil } + // There is one or more subdirs, so we need extract it to a temp - // dir and then read is as a charm dir. + // dir and then read it as a charm dir. tempDir, err := ioutil.TempDir("", "charm-extract") if err != nil { return errgo.Annotate(err, "cannot create temp directory") } defer os.RemoveAll(tempDir) - err = h.extractArchiveTo(zipr, rootDir, tempDir) - if err != nil { + if err := ziputil.Extract(zipr, tempDir, rootDir); err != nil { return errgo.Annotate(err, "cannot extract charm archive") } dir, err := charm.ReadDir(tempDir) if err != nil { return errgo.Annotate(err, "cannot read extracted archive") } + // Now repackage the dir as a bundle at the original path. if err := f.Truncate(0); err != nil { return err @@ -302,140 +272,36 @@ return nil } -// fixPath converts all forward and backslashes in path to the OS path -// separator and calls filepath.Clean before returning it. -func (h *charmsHandler) fixPath(path string) string { - sep := string(filepath.Separator) - p := strings.Replace(path, "\\", sep, -1) - return filepath.Clean(strings.Replace(p, "/", sep, -1)) -} - // findArchiveRootDir scans a zip archive and returns the rootDir of // the archive, the one containing metadata.yaml, config.yaml and // revision files, or an error if the archive appears invalid. func (h *charmsHandler) findArchiveRootDir(zipr *zip.Reader) (string, error) { - numFound := 0 - metadataFound := false // metadata.yaml is the only required file. - rootPath := "" - lookFor := []string{"metadata.yaml", "config.yaml", "revision"} - for _, fh := range zipr.File { - for _, fname := range lookFor { - dir, file := filepath.Split(h.fixPath(fh.Name)) - if file == fname { - if file == "metadata.yaml" { - metadataFound = true - } - numFound++ - if rootPath == "" { - rootPath = dir - } else if rootPath != dir { - return "", fmt.Errorf("invalid charm archive: expected all %v files in the same directory", lookFor) - } - if numFound == len(lookFor) { - return rootPath, nil - } - } - } + paths, err := ziputil.Find(zipr, "metadata.yaml") + if err != nil { + return "", err } - if !metadataFound { + switch len(paths) { + case 0: return "", fmt.Errorf("invalid charm archive: missing metadata.yaml") - } - return rootPath, nil -} - -// extractArchiveTo extracts an archive to the given destDir, removing -// the rootDir from each file, effectively reducing any nested subdirs -// to the root level. -func (h *charmsHandler) extractArchiveTo(zipr *zip.Reader, rootDir, destDir string) error { - for _, fh := range zipr.File { - err := h.extractSingleFile(fh, rootDir, destDir) - if err != nil { - return err + case 1: + default: + sort.Sort(byDepth(paths)) + if depth(paths[0]) == depth(paths[1]) { + return "", fmt.Errorf("invalid charm archive: ambiguous root directory") } } - return nil + return filepath.Dir(paths[0]), nil } -// extractSingleFile extracts the given zip file header, removing -// rootDir from the filename, to the destDir. -func (h *charmsHandler) extractSingleFile(fh *zip.File, rootDir, destDir string) error { - cleanName := h.fixPath(fh.Name) - relName, err := filepath.Rel(rootDir, cleanName) - if err != nil { - // Skip paths not relative to roo - return nil - } - if strings.Contains(relName, "..") || relName == "." { - // Skip current dir and paths outside rootDir. - return nil - } - dirName := filepath.Dir(relName) - f, err := fh.Open() - if err != nil { - return err - } - defer f.Close() +func depth(path string) int { + return strings.Count(path, "/") +} - mode := fh.Mode() - destPath := filepath.Join(destDir, relName) - if dirName != "" && mode&os.ModeDir != 0 { - err = os.MkdirAll(destPath, mode&0777) - if err != nil { - return err - } - return nil - } +type byDepth []string - if mode&os.ModeSymlink != 0 { - data, err := ioutil.ReadAll(f) - if err != nil { - return err - } - target := string(data) - if filepath.IsAbs(target) { - return fmt.Errorf("symlink %q is absolute: %q", cleanName, target) - } - p := filepath.Join(dirName, target) - if strings.Contains(p, "..") { - return fmt.Errorf("symlink %q links out of charm: %s", cleanName, target) - } - err = os.Symlink(target, destPath) - if err != nil { - return err - } - } - if dirName == "hooks" { - if mode&os.ModeType == 0 { - // Set all hooks executable (by owner) - mode = mode | 0100 - } - } - - // Check file type. - e := "file has an unknown type: %q" - switch mode & os.ModeType { - case os.ModeDir, os.ModeSymlink, 0: - // That's expected, it's ok. - e = "" - case os.ModeNamedPipe: - e = "file is a named pipe: %q" - case os.ModeSocket: - e = "file is a socket: %q" - case os.ModeDevice: - e = "file is a device: %q" - } - if e != "" { - return fmt.Errorf(e, destPath) - } - - out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, mode&0777) - if err != nil { - return fmt.Errorf("creating %q failed: %v", destPath, err) - } - defer out.Close() - _, err = io.Copy(out, f) - return err -} +func (d byDepth) Len() int { return len(d) } +func (d byDepth) Swap(i, j int) { d[i], d[j] = d[j], d[i] } +func (d byDepth) Less(i, j int) bool { return depth(d[i]) < depth(d[j]) } // repackageAndUploadCharm expands the given charm archive to a // temporary directoy, repackages it with the given curl's revision, @@ -465,6 +331,7 @@ if err != nil { return errgo.Annotate(err, "cannot read extracted charm") } + // Bundle the charm and calculate its sha256 hash at the // same time. hash := sha256.New() @@ -477,14 +344,12 @@ if err != nil { return errgo.Annotate(err, "cannot get charm file size") } - // Seek to the beginning so the subsequent Put will read - // the whole file again. + + // Now upload to provider storage. if _, err := repackagedArchive.Seek(0, 0); err != nil { return errgo.Annotate(err, "cannot rewind the charm file reader") } - - // Now upload to provider storage. - storage, err := envtesting.GetEnvironStorage(h.state) + storage, err := environs.GetStorage(h.state) if err != nil { return errgo.Annotate(err, "cannot access provider storage") } @@ -524,7 +389,7 @@ if file == "" { filePath = "" } else { - filePath = h.fixPath(file) + filePath = path.Clean(file) } // Prepare the bundle directories. @@ -547,7 +412,7 @@ // saves the corresponding zip archive to the given charmArchivePath. func (h *charmsHandler) downloadCharm(name, charmArchivePath string) error { // Get the provider storage. - storage, err := envtesting.GetEnvironStorage(h.state) + storage, err := environs.GetStorage(h.state) if err != nil { return errgo.Annotate(err, "cannot access provider storage") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charms_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/charms_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,6 @@ package apiserver_test import ( - "archive/zip" "bytes" "encoding/json" "fmt" @@ -16,27 +15,26 @@ "path/filepath" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" - envtesting "launchpad.net/juju-core/environs/testing" + "launchpad.net/juju-core/environs" jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) -type charmsSuite struct { +type authHttpSuite struct { jujutesting.JujuConnSuite - userTag string - password string + userTag string + password string + archiveContentType string } -var _ = gc.Suite(&charmsSuite{}) - -func (s *charmsSuite) SetUpTest(c *gc.C) { +func (s *authHttpSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) password, err := utils.RandomPassword() c.Assert(err, gc.IsNil) @@ -46,6 +44,49 @@ s.password = password } +func (s *authHttpSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, uri, body) + c.Assert(err, gc.IsNil) + if tag != "" && password != "" { + req.SetBasicAuth(tag, password) + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + return utils.GetNonValidatingHTTPClient().Do(req) +} + +func (s *authHttpSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) { + return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body) +} + +func (s *authHttpSuite) uploadRequest(c *gc.C, uri string, asZip bool, path string) (*http.Response, error) { + contentType := "application/octet-stream" + if asZip { + contentType = s.archiveContentType + } + + if path == "" { + return s.authRequest(c, "POST", uri, contentType, nil) + } + + file, err := os.Open(path) + c.Assert(err, gc.IsNil) + defer file.Close() + return s.authRequest(c, "POST", uri, contentType, file) +} + +type charmsSuite struct { + authHttpSuite +} + +var _ = gc.Suite(&charmsSuite{}) + +func (s *charmsSuite) SetUpSuite(c *gc.C) { + s.JujuConnSuite.SetUpSuite(c) + s.archiveContentType = "application/zip" +} + func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) { _, info, err := s.APIConn.Environ.StateInfo() c.Assert(err, gc.IsNil) @@ -169,7 +210,7 @@ expectedSHA256, _, err := utils.ReadSHA256(tempFile) c.Assert(err, gc.IsNil) name := charm.Quote(expectedURL.String()) - storage, err := envtesting.GetEnvironStorage(s.State) + storage, err := environs.GetStorage(s.State) c.Assert(err, gc.IsNil) expectedUploadURL, err := storage.URL(name) c.Assert(err, gc.IsNil) @@ -220,7 +261,7 @@ // should succeed, because it was repackaged during upload to // strip nested dirs. archiveName := strings.TrimPrefix(sch.BundleURL().RequestURI(), "/dummyenv/private/") - storage, err := envtesting.GetEnvironStorage(s.State) + storage, err := environs.GetStorage(s.State) c.Assert(err, gc.IsNil) reader, err := storage.Get(archiveName) c.Assert(err, gc.IsNil) @@ -329,7 +370,7 @@ } } -func (s *charmsSuite) TestGetListsFiles(c *gc.C) { +func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) { // Add the dummy charm. ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") _, err := s.uploadRequest( @@ -340,10 +381,9 @@ uri := s.charmsURI(c, "?url=local:quantal/dummy-1") resp, err := s.authRequest(c, "GET", uri, "", nil) c.Assert(err, gc.IsNil) - expectedFiles := []string{ - "revision", "config.yaml", "hooks/install", "metadata.yaml", - "src/hello.c", - } + manifest, err := ch.Manifest() + c.Assert(err, gc.IsNil) + expectedFiles := manifest.SortedValues() s.assertGetFileListResponse(c, resp, expectedFiles) ctype := resp.Header.Get("content-type") c.Assert(ctype, gc.Equals, "application/json") @@ -355,15 +395,14 @@ err := os.MkdirAll(cacheDir, 0755) c.Assert(err, gc.IsNil) - // Create and save the zip archive. - buffer := new(bytes.Buffer) - writer := zip.NewWriter(buffer) - file, err := writer.Create("utils.js") + // Create and save a bundle in it. + charmDir := coretesting.Charms.ClonedDir(c.MkDir(), "dummy") + testPath := filepath.Join(charmDir.Path, "utils.js") + contents := "// blah blah" + err = ioutil.WriteFile(testPath, []byte(contents), 0755) c.Assert(err, gc.IsNil) - contents := "// these are the voyages" - _, err = file.Write([]byte(contents)) - c.Assert(err, gc.IsNil) - err = writer.Close() + var buffer bytes.Buffer + err = charmDir.BundleTo(&buffer) c.Assert(err, gc.IsNil) charmArchivePath := filepath.Join( cacheDir, charm.Quote("local:trusty/django-42")+".zip") @@ -383,38 +422,6 @@ return "https://" + info.Addrs[0] + "/charms" + query } -func (s *charmsSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest(method, uri, body) - c.Assert(err, gc.IsNil) - if tag != "" && password != "" { - req.SetBasicAuth(tag, password) - } - if contentType != "" { - req.Header.Set("Content-Type", contentType) - } - return utils.GetNonValidatingHTTPClient().Do(req) -} - -func (s *charmsSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) { - return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body) -} - -func (s *charmsSuite) uploadRequest(c *gc.C, uri string, asZip bool, path string) (*http.Response, error) { - contentType := "application/octet-stream" - if asZip { - contentType = "application/zip" - } - - if path == "" { - return s.authRequest(c, "POST", uri, contentType, nil) - } - - file, err := os.Open(path) - c.Assert(err, gc.IsNil) - defer file.Close() - return s.authRequest(c, "POST", uri, contentType, file) -} - func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) { body := assertResponse(c, resp, http.StatusOK, "application/json") charmResponse := jsonResponse(c, body) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/api_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/api_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/api_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/api_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ stdtesting "testing" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -20,7 +21,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func TestAll(t *stdtesting.T) { @@ -262,7 +262,7 @@ add := func(e state.Entity) { entities = append(entities, e.Tag()) } - u, err := s.State.User("admin") + u, err := s.State.User(state.AdminUser) c.Assert(err, gc.IsNil) setDefaultPassword(c, u) add(u) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/client.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/client.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,14 +10,13 @@ "strings" "github.com/errgo/errgo" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/charm" - coreCloudinit "launchpad.net/juju-core/cloudinit" - "launchpad.net/juju-core/cloudinit/sshinit" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/environs/manual" + envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju" @@ -27,6 +26,7 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" "launchpad.net/juju-core/state/statecmd" + coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" ) @@ -184,6 +184,34 @@ return results, fmt.Errorf("unknown unit or machine %q", p.Target) } +// PrivateAddress implements the server side of Client.PrivateAddress. +func (c *Client) PrivateAddress(p params.PrivateAddress) (results params.PrivateAddressResults, err error) { + switch { + case names.IsMachine(p.Target): + machine, err := c.api.state.Machine(p.Target) + if err != nil { + return results, err + } + addr := instance.SelectInternalAddress(machine.Addresses(), false) + if addr == "" { + return results, fmt.Errorf("machine %q has no internal address", machine) + } + return params.PrivateAddressResults{PrivateAddress: addr}, nil + + case names.IsUnit(p.Target): + unit, err := c.api.state.Unit(p.Target) + if err != nil { + return results, err + } + addr, ok := unit.PrivateAddress() + if !ok { + return results, fmt.Errorf("unit %q has no internal address", unit) + } + return params.PrivateAddressResults{PrivateAddress: addr}, nil + } + return results, fmt.Errorf("unknown unit or machine %q", p.Target) +} + // ServiceExpose changes the juju-managed firewall to expose any ports that // were also explicitly marked by units as open. func (c *Client) ServiceExpose(args params.ServiceExpose) error { @@ -609,15 +637,7 @@ return result, err } mcfg.DisablePackageCommands = args.DisablePackageCommands - cloudcfg := coreCloudinit.New() - if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { - return result, err - } - // ProvisioningScript is run on an existing machine; - // we explicitly disable apt_upgrade so as not to - // trample the machine's existing configuration. - cloudcfg.SetAptUpgrade(false) - result.Script, err = sshinit.ConfigureScript(cloudcfg) + result.Script, err = manual.ProvisioningScript(mcfg) return result, err } @@ -769,38 +789,23 @@ // EnvironmentSet implements the server-side part of the // set-environment CLI command. func (c *Client) EnvironmentSet(args params.EnvironmentSet) error { - // TODO(dimitern,thumper): 2013-11-06 bug #1167616 - // SetEnvironConfig should take both new and old configs. - - // Get the existing environment config from the state. - oldConfig, err := c.api.state.EnvironConfig() - if err != nil { - return err - } // Make sure we don't allow changing agent-version. - if v, found := args.Config["agent-version"]; found { - oldVersion, _ := oldConfig.AgentVersion() - if v != oldVersion.String() { - return fmt.Errorf("agent-version cannot be changed") + checkAgentVersion := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { + if v, found := updateAttrs["agent-version"]; found { + oldVersion, _ := oldConfig.AgentVersion() + if v != oldVersion.String() { + return fmt.Errorf("agent-version cannot be changed") + } } + return nil } - // Apply the attributes specified for the command to the state config. - newConfig, err := oldConfig.Apply(args.Config) - if err != nil { - return err - } - env, err := environs.New(oldConfig) - if err != nil { - return err - } - // Now validate this new config against the existing config via the provider. - provider := env.Provider() - newProviderConfig, err := provider.Validate(newConfig, oldConfig) - if err != nil { - return err - } - // Now try to apply the new validated config. - return c.api.state.SetEnvironConfig(newProviderConfig, oldConfig) + + // TODO(waigani) 2014-3-11 #1167616 + // Add a txn retry loop to ensure that the settings on disk have not + // changed underneath us. + + return c.api.state.UpdateEnvironConfig(args.Config, nil, checkAgentVersion) + } // SetEnvironAgentVersion sets the environment agent version. @@ -808,6 +813,27 @@ return c.api.state.SetEnvironAgentVersion(args.Version) } +// FindTools returns a List containing all tools matching the given parameters. +func (c *Client) FindTools(args params.FindToolsParams) (params.FindToolsResults, error) { + result := params.FindToolsResults{} + // Get the existing environment config from the state. + envConfig, err := c.api.state.EnvironConfig() + if err != nil { + return result, err + } + env, err := environs.New(envConfig) + if err != nil { + return result, err + } + filter := coretools.Filter{ + Arch: args.Arch, + Series: args.Series, + } + result.List, err = envtools.FindTools(env, args.MajorVersion, args.MinorVersion, filter, envtools.DoNotAllowRetry) + result.Error = common.ServerError(err) + return result, nil +} + func destroyErr(desc string, ids, errs []string) error { if len(errs) == 0 { return nil @@ -849,7 +875,7 @@ if err != nil { return err } - store := config.AuthorizeCharmRepo(CharmStore, envConfig) + store := config.SpecializeCharmRepo(CharmStore, envConfig) downloadedCharm, err := store.Get(charmURL) if err != nil { return errgo.Annotatef(err, "cannot download charm %q", charmURL.String()) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/client_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/client_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,16 +11,16 @@ "sync" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" - coreCloudinit "launchpad.net/juju-core/cloudinit" - "launchpad.net/juju-core/cloudinit/sshinit" "launchpad.net/juju-core/constraints" - "launchpad.net/juju-core/environs/cloudinit" + "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" - "launchpad.net/juju-core/environs/storage" - envtesting "launchpad.net/juju-core/environs/testing" + "launchpad.net/juju-core/environs/manual" + envstorage "launchpad.net/juju-core/environs/storage" + ttesting "launchpad.net/juju-core/environs/tools/testing" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/provider/dummy" @@ -30,7 +30,6 @@ "launchpad.net/juju-core/state/apiserver/client" "launchpad.net/juju-core/state/statecmd" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -1414,6 +1413,66 @@ c.Assert(addr, gc.Equals, "127.0.0.1") } +func (s *clientSuite) TestClientPrivateAddressErrors(c *gc.C) { + s.setUpScenario(c) + _, err := s.APIState.Client().PrivateAddress("wordpress") + c.Assert(err, gc.ErrorMatches, `unknown unit or machine "wordpress"`) + _, err = s.APIState.Client().PrivateAddress("0") + c.Assert(err, gc.ErrorMatches, `machine "0" has no internal address`) + _, err = s.APIState.Client().PrivateAddress("wordpress/0") + c.Assert(err, gc.ErrorMatches, `unit "wordpress/0" has no internal address`) +} + +func (s *clientSuite) TestClientPrivateAddressMachine(c *gc.C) { + s.setUpScenario(c) + + // Internally, instance.SelectInternalAddress is used; the public + // address if no cloud-local one is available. + m1, err := s.State.Machine("1") + c.Assert(err, gc.IsNil) + cloudLocalAddress := instance.NewAddress("cloudlocal") + cloudLocalAddress.NetworkScope = instance.NetworkCloudLocal + publicAddress := instance.NewAddress("public") + publicAddress.NetworkScope = instance.NetworkCloudLocal + err = m1.SetAddresses([]instance.Address{publicAddress}) + c.Assert(err, gc.IsNil) + addr, err := s.APIState.Client().PrivateAddress("1") + c.Assert(err, gc.IsNil) + c.Assert(addr, gc.Equals, "public") + err = m1.SetAddresses([]instance.Address{cloudLocalAddress, publicAddress}) + addr, err = s.APIState.Client().PrivateAddress("1") + c.Assert(err, gc.IsNil) + c.Assert(addr, gc.Equals, "cloudlocal") +} + +func (s *clientSuite) TestClientPrivateAddressUnitWithMachine(c *gc.C) { + s.setUpScenario(c) + + // Private address of unit is taken from its machine + // (if its machine has addresses). + m1, err := s.State.Machine("1") + publicAddress := instance.NewAddress("public") + publicAddress.NetworkScope = instance.NetworkCloudLocal + err = m1.SetAddresses([]instance.Address{publicAddress}) + c.Assert(err, gc.IsNil) + addr, err := s.APIState.Client().PrivateAddress("wordpress/0") + c.Assert(err, gc.IsNil) + c.Assert(addr, gc.Equals, "public") +} + +func (s *clientSuite) TestClientPrivateAddressUnitWithoutMachine(c *gc.C) { + s.setUpScenario(c) + // If the unit's machine has no addresses, the public address + // comes from the unit's document. + u, err := s.State.Unit("wordpress/1") + c.Assert(err, gc.IsNil) + err = u.SetPrivateAddress("127.0.0.1") + c.Assert(err, gc.IsNil) + addr, err := s.APIState.Client().PrivateAddress("wordpress/1") + c.Assert(err, gc.IsNil) + c.Assert(addr, gc.Equals, "127.0.0.1") +} + func (s *clientSuite) TestClientEnvironmentGet(c *gc.C) { envConfig, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) @@ -1475,6 +1534,18 @@ c.Assert(err, gc.IsNil) } +func (s *clientSuite) TestClientFindTools(c *gc.C) { + result, err := s.APIState.Client().FindTools(2, -1, "", "") + c.Assert(err, gc.IsNil) + c.Assert(result.Error, jc.Satisfies, params.IsCodeNotFound) + ttesting.UploadToStorage(c, s.Conn.Environ.Storage(), version.MustParseBinary("2.12.0-precise-amd64")) + result, err = s.APIState.Client().FindTools(2, 12, "precise", "amd64") + c.Assert(err, gc.IsNil) + c.Assert(result.Error, gc.IsNil) + c.Assert(result.List, gc.HasLen, 1) + c.Assert(result.List[0].Version, gc.Equals, version.MustParseBinary("2.12.0-precise-amd64")) +} + func (s *clientSuite) checkMachine(c *gc.C, id, series, cons string) { // Ensure the machine was actually created. machine, err := s.BackingState.Machine(id) @@ -1682,11 +1753,7 @@ c.Assert(err, gc.IsNil) mcfg, err := statecmd.MachineConfig(s.State, machineId, apiParams.Nonce, "") c.Assert(err, gc.IsNil) - cloudcfg := coreCloudinit.New() - err = cloudinit.ConfigureJuju(mcfg, cloudcfg) - c.Assert(err, gc.IsNil) - cloudcfg.SetAptUpgrade(false) - sshinitScript, err := sshinit.ConfigureScript(cloudcfg) + sshinitScript, err := manual.ProvisioningScript(mcfg) c.Assert(err, gc.IsNil) // ProvisioningScript internally calls MachineConfig, // which allocates a new, random password. Everything @@ -1730,20 +1797,13 @@ } } -func (s *clientSuite) TestClientAuthorizeStoreOnDeployServiceSetCharmAndAddCharm(c *gc.C) { +func (s *clientSuite) TestClientSpecializeStoreOnDeployServiceSetCharmAndAddCharm(c *gc.C) { store, restore := makeMockCharmStore() defer restore() - oldConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - - attrs := coretesting.Attrs(oldConfig.AllAttrs()) - attrs = attrs.Merge(coretesting.Attrs{"charm-store-auth": "token=value"}) - - cfg, err := config.New(config.NoDefaults, attrs) - c.Assert(err, gc.IsNil) - - err = s.State.SetEnvironConfig(cfg, oldConfig) + attrs := map[string]interface{}{"charm-store-auth": "token=value", + "test-mode": true} + err := s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) curl, _ := addCharm(c, store, "dummy") @@ -1754,6 +1814,7 @@ // check that the store's auth attributes were set c.Assert(store.AuthAttrs, gc.Equals, "token=value") + c.Assert(store.TestMode, gc.Equals, true) store.AuthAttrs = "" @@ -1856,7 +1917,7 @@ // contains the correct data. sch, err := s.State.Charm(curl) c.Assert(err, gc.IsNil) - storage, err := envtesting.GetEnvironStorage(s.State) + storage, err := environs.GetStorage(s.State) c.Assert(err, gc.IsNil) uploads, err := storage.List(fmt.Sprintf("%s-%d-", curl.Name, curl.Revision)) c.Assert(err, gc.IsNil) @@ -1922,7 +1983,7 @@ } } -func (s *clientSuite) assertUploaded(c *gc.C, storage storage.Storage, bundleURL *url.URL, expectedSHA256 string) { +func (s *clientSuite) assertUploaded(c *gc.C, storage envstorage.Storage, bundleURL *url.URL, expectedSHA256 string) { archiveName := getArchiveName(bundleURL) reader, err := storage.Get(archiveName) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/destroy_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/destroy_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/destroy_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/destroy_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" ) type destroyEnvironmentSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/run_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/run_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/client/run_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/run_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "path/filepath" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/client" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils/exec" "launchpad.net/juju-core/utils/ssh" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/addresses.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/addresses.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,7 +11,7 @@ // state server addresses and the CA public certificate. type AddressAndCertGetter interface { Addresses() ([]string, error) - APIAddresses() ([]string, error) + APIAddressesFromMachines() ([]string, error) CACert() []byte } @@ -28,7 +28,7 @@ // APIAddresses returns the list of addresses used to connect to the API. func (a *APIAddresser) APIAddresses() (params.StringsResult, error) { - addrs, err := a.getter.APIAddresses() + addrs, err := a.getter.APIAddressesFromMachines() if err != nil { return params.StringsResult{}, err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -55,7 +55,7 @@ return []string{"addresses:1", "addresses:2"}, nil } -func (fakeAddresses) APIAddresses() ([]string, error) { +func (fakeAddresses) APIAddressesFromMachines() ([]string, error) { return []string{"apiaddresses:1", "apiaddresses:2"}, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/environmachineswatcher_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environmachineswatcher_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/environmachineswatcher_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environmachineswatcher_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,12 +6,12 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" - jc "launchpad.net/juju-core/testing/checkers" ) type environMachinesWatcherSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/errors_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/errors_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/errors_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/errors_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( stderrors "errors" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/instanceidgetter_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/instanceidgetter_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/instanceidgetter_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/instanceidgetter_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type instanceIdGetterSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/password.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/password.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/password.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/password.go 2014-03-20 12:52:38.000000000 +0000 @@ -26,7 +26,7 @@ } // SetPasswords sets the given password for each supplied entity, if possible. -func (pc *PasswordChanger) SetPasswords(args params.PasswordChanges) (params.ErrorResults, error) { +func (pc *PasswordChanger) SetPasswords(args params.EntityPasswords) (params.ErrorResults, error) { result := params.ErrorResults{ Results: make([]params.ErrorResult, len(args.Changes)), } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/password_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/password_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/password_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/password_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type passwordSuite struct{} @@ -117,15 +117,15 @@ }, nil } pc := common.NewPasswordChanger(st, getCanChange) - var changes []params.PasswordChange + var changes []params.EntityPassword for i := 0; i < len(st.entities); i++ { tag := fmt.Sprintf("x%d", i) - changes = append(changes, params.PasswordChange{ + changes = append(changes, params.EntityPassword{ Tag: tag, Password: fmt.Sprintf("%spass", tag), }) } - results, err := pc.SetPasswords(params.PasswordChanges{ + results, err := pc.SetPasswords(params.EntityPasswords{ Changes: changes, }) c.Assert(err, gc.IsNil) @@ -156,15 +156,15 @@ return nil, fmt.Errorf("splat") } pc := common.NewPasswordChanger(&fakeState{}, getCanChange) - var changes []params.PasswordChange + var changes []params.EntityPassword for i := 0; i < 4; i++ { tag := fmt.Sprintf("x%d", i) - changes = append(changes, params.PasswordChange{ + changes = append(changes, params.EntityPassword{ Tag: tag, Password: fmt.Sprintf("%spass", tag), }) } - _, err := pc.SetPasswords(params.PasswordChanges{Changes: changes}) + _, err := pc.SetPasswords(params.EntityPasswords{Changes: changes}) c.Assert(err, gc.ErrorMatches, "splat") } @@ -173,7 +173,7 @@ return nil, fmt.Errorf("splat") } pc := common.NewPasswordChanger(&fakeState{}, getCanChange) - result, err := pc.SetPasswords(params.PasswordChanges{}) + result, err := pc.SetPasswords(params.EntityPasswords{}) c.Assert(err, gc.IsNil) c.Assert(result.Results, gc.HasLen, 0) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package testing import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/environs" @@ -11,7 +12,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) const ( diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/tools_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/tools_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/tools_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/tools_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/testing" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/unitswatcher_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/unitswatcher_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/common/unitswatcher_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/unitswatcher_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,13 +6,13 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type unitsWatcherSuite struct{} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/deployer/deployer_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/deployer/deployer_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/deployer/deployer_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/deployer/deployer_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -147,8 +147,8 @@ } func (s *deployerSuite) TestSetPasswords(c *gc.C) { - args := params.PasswordChanges{ - Changes: []params.PasswordChange{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: "unit-mysql-0", Password: "xxx-12345678901234567890"}, {Tag: "unit-mysql-1", Password: "yyy-12345678901234567890"}, {Tag: "unit-logging-0", Password: "zzz-12345678901234567890"}, @@ -182,8 +182,8 @@ err = s.subordinate0.Refresh() c.Assert(errors.IsNotFoundError(err), gc.Equals, true) - results, err = s.deployer.SetPasswords(params.PasswordChanges{ - Changes: []params.PasswordChange{ + results, err = s.deployer.SetPasswords(params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: "unit-logging-0", Password: "blah-12345678901234567890"}, }, }) @@ -321,7 +321,7 @@ }) c.Assert(err, gc.IsNil) - apiAddresses, err := s.State.APIAddresses() + apiAddresses, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) result, err := s.deployer.APIAddresses() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/firewaller/firewaller_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/firewaller/firewaller_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/firewaller/firewaller_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/firewaller/firewaller_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" @@ -19,7 +20,6 @@ apiservertesting "launchpad.net/juju-core/state/apiserver/testing" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func Test(t *stdtesting.T) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/httphandler.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/httphandler.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/httphandler.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/httphandler.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,65 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + + "launchpad.net/juju-core/names" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/state/apiserver/common" +) + +// errorSender implementations send errors back to the caller. +type errorSender interface { + sendError(w http.ResponseWriter, statusCode int, message string) error +} + +// httpHandler handles http requests through HTTPS in the API server. +type httpHandler struct { + // Structs which embed httpHandler provide their own errorSender implementation. + errorSender + state *state.State +} + +// authenticate parses HTTP basic authentication and authorizes the +// request by looking up the provided tag and password against state. +func (h *httpHandler) authenticate(r *http.Request) error { + parts := strings.Fields(r.Header.Get("Authorization")) + if len(parts) != 2 || parts[0] != "Basic" { + // Invalid header format or no header provided. + return fmt.Errorf("invalid request format") + } + // Challenge is a base64-encoded "tag:pass" string. + // See RFC 2617, Section 2. + challenge, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return fmt.Errorf("invalid request format") + } + tagPass := strings.SplitN(string(challenge), ":", 2) + if len(tagPass) != 2 { + return fmt.Errorf("invalid request format") + } + // Only allow users, not agents. + _, _, err = names.ParseTag(tagPass[0], names.UserTagKind) + if err != nil { + return common.ErrBadCreds + } + // Ensure the credentials are correct. + _, err = checkCreds(h.state, params.Creds{ + AuthTag: tagPass[0], + Password: tagPass[1], + }) + return err +} + +// authError sends an unauthorized error. +func (h *httpHandler) authError(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) + h.sendError(w, http.StatusUnauthorized, "unauthorized") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "fmt" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/errors" @@ -144,14 +144,14 @@ return keyInfo } -func (api *KeyManagerAPI) writeSSHKeys(currentConfig *config.Config, sshKeys []string) error { +func (api *KeyManagerAPI) writeSSHKeys(sshKeys []string) error { // Write out the new keys. keyStr := strings.Join(sshKeys, "\n") - newConfig, err := currentConfig.Apply(map[string]interface{}{config.AuthKeysConfig: keyStr}) - if err != nil { - return fmt.Errorf("creating new environ config: %v", err) - } - err = api.state.SetEnvironConfig(newConfig, currentConfig) + attrs := map[string]interface{}{config.AuthKeysConfig: keyStr} + // TODO(waigani) 2014-03-17 bug #1293324 + // Pass in validation to ensure SSH keys + // have not changed underfoot + err := api.state.UpdateEnvironConfig(attrs, nil, nil) if err != nil { return fmt.Errorf("writing environ config: %v", err) } @@ -159,12 +159,12 @@ } // currentKeyDataForAdd gathers data used when adding ssh keys. -func (api *KeyManagerAPI) currentKeyDataForAdd() (keys []string, fingerprints *set.Strings, cfg *config.Config, err error) { +func (api *KeyManagerAPI) currentKeyDataForAdd() (keys []string, fingerprints *set.Strings, err error) { fp := set.NewStrings() fingerprints = &fp - cfg, err = api.state.EnvironConfig() + cfg, err := api.state.EnvironConfig() if err != nil { - return nil, nil, nil, err + return nil, nil, fmt.Errorf("reading current key data: %v", err) } keys = ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) for _, key := range keys { @@ -174,7 +174,7 @@ } fingerprints.Add(fingerprint) } - return keys, fingerprints, cfg, nil + return keys, fingerprints, nil } // AddKeys adds new authorised ssh keys for the specified user. @@ -195,7 +195,7 @@ } // For now, authorised keys are global, common to all users. - sshKeys, currentFingerprints, currentConfig, err := api.currentKeyDataForAdd() + sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() if err != nil { return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) } @@ -214,7 +214,7 @@ } sshKeys = append(sshKeys, key) } - err = api.writeSSHKeys(currentConfig, sshKeys) + err = api.writeSSHKeys(sshKeys) if err != nil { return params.ErrorResults{}, common.ServerError(err) } @@ -278,7 +278,7 @@ } // For now, authorised keys are global, common to all users. - sshKeys, currentFingerprints, currentConfig, err := api.currentKeyDataForAdd() + sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() if err != nil { return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) } @@ -297,7 +297,7 @@ } sshKeys = append(sshKeys, keyInfo.key) } - err = api.writeSSHKeys(currentConfig, sshKeys) + err = api.writeSSHKeys(sshKeys) if err != nil { return params.ErrorResults{}, common.ServerError(err) } @@ -306,13 +306,13 @@ // currentKeyDataForAdd gathers data used when deleting ssh keys. func (api *KeyManagerAPI) currentKeyDataForDelete() ( - keys map[string]string, invalidKeys []string, comments map[string]string, cfg *config.Config, err error) { + keys map[string]string, invalidKeys []string, comments map[string]string, err error) { - // For now, authorised keys are global, common to all users. - cfg, err = api.state.EnvironConfig() + cfg, err := api.state.EnvironConfig() if err != nil { - return nil, nil, nil, nil, fmt.Errorf("reading current key data: %v", err) + return nil, nil, nil, fmt.Errorf("reading current key data: %v", err) } + // For now, authorised keys are global, common to all users. existingSSHKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) // Build up a map of keys indexed by fingerprint, and fingerprints indexed by comment @@ -332,7 +332,7 @@ comments[comment] = fingerprint } } - return keys, invalidKeys, comments, cfg, nil + return keys, invalidKeys, comments, nil } // DeleteKeys deletes the authorised ssh keys for the specified user. @@ -352,7 +352,7 @@ return params.ErrorResults{}, common.ServerError(common.ErrPerm) } - sshKeys, invalidKeys, keyComments, currentConfig, err := api.currentKeyDataForDelete() + sshKeys, invalidKeys, keyComments, err := api.currentKeyDataForDelete() if err != nil { return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) } @@ -382,7 +382,7 @@ return params.ErrorResults{}, common.ServerError(stderrors.New("cannot delete all keys")) } - err = api.writeSSHKeys(currentConfig, keysToWrite) + err = api.writeSSHKeys(keysToWrite) if err != nil { return params.ErrorResults{}, common.ServerError(err) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keymanager/keymanager_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,12 +10,12 @@ gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" "launchpad.net/juju-core/state/apiserver/keymanager" keymanagertesting "launchpad.net/juju-core/state/apiserver/keymanager/testing" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" - statetesting "launchpad.net/juju-core/state/testing" "launchpad.net/juju-core/utils/ssh" sshtesting "launchpad.net/juju-core/utils/ssh/testing" ) @@ -77,7 +77,7 @@ } func (s *keyManagerSuite) setAuthorisedKeys(c *gc.C, keys string) { - err := statetesting.UpdateConfig(s.State, map[string]interface{}{"authorized-keys": keys}) + err := s.State.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keys}, nil, nil) c.Assert(err, gc.IsNil) envConfig, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) @@ -91,7 +91,7 @@ args := params.ListSSHKeys{ Entities: params.Entities{[]params.Entity{ - {Tag: "admin"}, + {Tag: state.AdminUser}, {Tag: "invalid"}, }}, Mode: ssh.FullKeys, @@ -121,7 +121,7 @@ newKey := sshtesting.ValidKeyThree.Key + " newuser@host" args := params.ModifyUserSSHKeys{ - User: "admin", + User: state.AdminUser, Keys: []string{key2, newKey, "invalid-key"}, } results, err := s.keymanager.AddKeys(args) @@ -192,7 +192,7 @@ s.setAuthorisedKeys(c, strings.Join(initialKeys, "\n")) args := params.ModifyUserSSHKeys{ - User: "admin", + User: state.AdminUser, Keys: []string{sshtesting.ValidKeyTwo.Fingerprint, sshtesting.ValidKeyThree.Fingerprint, "invalid-key"}, } results, err := s.keymanager.DeleteKeys(args) @@ -214,7 +214,7 @@ s.setAuthorisedKeys(c, strings.Join(initialKeys, "\n")) args := params.ModifyUserSSHKeys{ - User: "admin", + User: state.AdminUser, Keys: []string{sshtesting.ValidKeyTwo.Fingerprint, "user@host"}, } _, err := s.keymanager.DeleteKeys(args) @@ -264,7 +264,7 @@ s.setAuthorisedKeys(c, strings.Join(initialKeys, "\n")) args := params.ModifyUserSSHKeys{ - User: "admin", + User: state.AdminUser, Keys: []string{"lp:existing", "lp:validuser", "invalid-key"}, } results, err := s.keymanager.ImportKeys(args) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keyupdater/authorisedkeys_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keyupdater/authorisedkeys_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/keyupdater/authorisedkeys_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/keyupdater/authorisedkeys_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -73,7 +73,7 @@ } func (s *authorisedKeysSuite) setAuthorizedKeys(c *gc.C, keys string) { - err := statetesting.UpdateConfig(s.State, map[string]interface{}{"authorized-keys": keys}) + err := s.State.UpdateEnvironConfig(map[string]interface{}{"authorized-keys": keys}, nil, nil) c.Assert(err, gc.IsNil) envConfig, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/logger/logger.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/logger/logger.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/logger/logger.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/logger/logger.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,7 @@ package logger import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/logger/logger_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/logger/logger_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/logger/logger_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/logger/logger_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -74,7 +74,7 @@ } func (s *loggerSuite) setLoggingConfig(c *gc.C, loggingConfig string) { - err := statetesting.UpdateConfig(s.State, map[string]interface{}{"logging-config": loggingConfig}) + err := s.State.UpdateEnvironConfig(map[string]interface{}{"logging-config": loggingConfig}, nil, nil) c.Assert(err, gc.IsNil) envConfig, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/login_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/login_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/login_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/login_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,8 @@ package apiserver_test import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" @@ -13,7 +14,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) @@ -99,6 +99,31 @@ } } +func (s *loginSuite) TestLoginAsDeactivatedUser(c *gc.C) { + info, cleanup := s.setupServer(c) + defer cleanup() + + info.Tag = "" + info.Password = "" + st, err := api.Open(info, fastDialOpts) + c.Assert(err, gc.IsNil) + defer st.Close() + u, err := s.State.AddUser("inactive", "password") + c.Assert(err, gc.IsNil) + err = u.Deactivate() + c.Assert(err, gc.IsNil) + + _, err = st.Client().Status([]string{}) + c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`) + + // Since these are user login tests, the nonce is empty. + err = st.Login("user-inactive", "password", "") + c.Assert(err, gc.ErrorMatches, "invalid entity name or password") + + _, err = st.Client().Status([]string{}) + c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`) +} + func (s *loginSuite) TestLoginSetsLogIdentifier(c *gc.C) { info, cleanup := s.setupServer(c) defer cleanup() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/pinger_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/pinger_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/pinger_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/pinger_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/testing" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -22,7 +23,6 @@ apiservertesting "launchpad.net/juju-core/state/apiserver/testing" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/version" ) @@ -114,8 +114,8 @@ } func (s *withoutStateServerSuite) TestSetPasswords(c *gc.C) { - args := params.PasswordChanges{ - Changes: []params.PasswordChange{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: s.machines[0].Tag(), Password: "xxx0-1234567890123457890"}, {Tag: s.machines[1].Tag(), Password: "xxx1-1234567890123457890"}, {Tag: s.machines[2].Tag(), Password: "xxx2-1234567890123457890"}, @@ -148,8 +148,8 @@ } func (s *withoutStateServerSuite) TestShortSetPasswords(c *gc.C) { - args := params.PasswordChanges{ - Changes: []params.PasswordChange{ + args := params.EntityPasswords{ + Changes: []params.EntityPassword{ {Tag: s.machines[1].Tag(), Password: "xxx1"}, }, } @@ -683,13 +683,10 @@ } func (s *withoutStateServerSuite) TestContainerConfig(c *gc.C) { - cfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - newCfg, err := cfg.Apply(map[string]interface{}{ + attrs := map[string]interface{}{ "http-proxy": "http://proxy.example.com:9000", - }) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(newCfg, cfg) + } + err := s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) expectedProxy := osenv.ProxySettings{ Http: "http://proxy.example.com:9000", @@ -837,7 +834,7 @@ } func (s *withStateServerSuite) TestAPIAddresses(c *gc.C) { - addrs, err := s.State.APIAddresses() + addrs, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) result, err := s.provisioner.APIAddresses() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/root.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/root.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/root.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/root.go 2014-03-20 12:52:38.000000000 +0000 @@ -27,6 +27,7 @@ "launchpad.net/juju-core/state/apiserver/rsyslog" "launchpad.net/juju-core/state/apiserver/uniter" "launchpad.net/juju-core/state/apiserver/upgrader" + "launchpad.net/juju-core/state/apiserver/usermanager" "launchpad.net/juju-core/state/multiwatcher" ) @@ -108,6 +109,16 @@ return keymanager.NewKeyManagerAPI(r.srv.state, r.resources, r) } +// UserManager returns an object that provides access to the UserManager API +// facade. The id argument is reserved for future use and currently +// needs to be empty +func (r *srvRoot) UserManager(id string) (*usermanager.UserManagerAPI, error) { + if id != "" { + return nil, common.ErrBadId + } + return usermanager.NewUserManagerAPI(r.srv.state, r) +} + // Machiner returns an object that provides access to the Machiner API // facade. The id argument is reserved for future use and currently // needs to be empty. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog.go 2014-03-20 12:52:38.000000000 +0000 @@ -40,17 +40,9 @@ result.Error = common.ServerError(err) return result, nil } - old, err := api.st.EnvironConfig() - if err != nil { - return params.ErrorResult{}, err - } - cfg, err := old.Apply(map[string]interface{}{"rsyslog-ca-cert": string(args.CACert)}) - if err != nil { + attrs := map[string]interface{}{"rsyslog-ca-cert": string(args.CACert)} + if err := api.st.UpdateEnvironConfig(attrs, nil, nil); err != nil { result.Error = common.ServerError(err) - } else { - if err := api.st.SetEnvironConfig(cfg, old); err != nil { - result.Error = common.ServerError(err) - } } return result, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/rsyslog/rsyslog_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "encoding/pem" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/testing" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/state/apiserver/rsyslog" apiservertesting "launchpad.net/juju-core/state/apiserver/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type rsyslogSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/server_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/server_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/server_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/server_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ stdtesting "testing" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" @@ -17,7 +18,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/tools.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/tools.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/tools.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/tools.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,194 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "strings" + + "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/filestorage" + "launchpad.net/juju-core/environs/sync" + envtools "launchpad.net/juju-core/environs/tools" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/state/apiserver/common" + "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/version" +) + +// toolsHandler handles tool upload through HTTPS in the API server. +type toolsHandler struct { + httpHandler +} + +func (h *toolsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := h.authenticate(r); err != nil { + h.authError(w) + return + } + + switch r.Method { + case "POST": + // Add a local charm to the store provider. + // Requires a "series" query specifying the series to use for the charm. + agentTools, disableSSLHostnameVerification, err := h.processPost(r) + if err != nil { + h.sendError(w, http.StatusBadRequest, err.Error()) + return + } + h.sendJSON(w, http.StatusOK, ¶ms.ToolsResult{ + Tools: agentTools, + DisableSSLHostnameVerification: disableSSLHostnameVerification, + }) + default: + h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsupported method: %q", r.Method)) + } +} + +// sendJSON sends a JSON-encoded response to the client. +func (h *toolsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.ToolsResult) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + body, err := json.Marshal(response) + if err != nil { + return err + } + w.Write(body) + return nil +} + +// sendError sends a JSON-encoded error response. +func (h *toolsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error { + err := common.ServerError(fmt.Errorf(message)) + return h.sendJSON(w, statusCode, ¶ms.ToolsResult{Error: err}) +} + +// processPost handles a charm upload POST request after authentication. +func (h *toolsHandler) processPost(r *http.Request) (*tools.Tools, bool, error) { + query := r.URL.Query() + binaryVersionParam := query.Get("binaryVersion") + if binaryVersionParam == "" { + return nil, false, fmt.Errorf("expected binaryVersion argument") + } + toolsVersion, err := version.ParseBinary(binaryVersionParam) + if err != nil { + return nil, false, fmt.Errorf("invalid tools version %q: %v", binaryVersionParam, err) + } + var fakeSeries []string + seriesParam := query.Get("series") + if seriesParam != "" { + fakeSeries = strings.Split(seriesParam, ",") + } + logger.Debugf("request to upload tools %s for series %q", toolsVersion, seriesParam) + // Make sure the content type is x-tar-gz. + contentType := r.Header.Get("Content-Type") + if contentType != "application/x-tar-gz" { + return nil, false, fmt.Errorf("expected Content-Type: application/x-tar-gz, got: %v", contentType) + } + return h.handleUpload(r.Body, toolsVersion, fakeSeries...) +} + +// handleUpload uploads the tools data from the reader to env storage as the specified version. +func (h *toolsHandler) handleUpload(r io.Reader, toolsVersion version.Binary, fakeSeries ...string) (*tools.Tools, bool, error) { + // Set up a local temp directory for the tools tarball. + tmpDir, err := ioutil.TempDir("", "juju-upload-tools-") + if err != nil { + return nil, false, fmt.Errorf("cannot create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + toolsFilename := envtools.StorageName(toolsVersion) + toolsDir := path.Dir(toolsFilename) + fullToolsDir := path.Join(tmpDir, toolsDir) + err = os.MkdirAll(fullToolsDir, 0700) + if err != nil { + return nil, false, fmt.Errorf("cannot create tools dir %s: %v", toolsDir, err) + } + + // Read the tools tarball from the request, calculating the sha256 along the way. + fullToolsFilename := path.Join(tmpDir, toolsFilename) + toolsFile, err := os.Create(fullToolsFilename) + if err != nil { + return nil, false, fmt.Errorf("cannot create tools file %s: %v", fullToolsFilename, err) + } + logger.Debugf("saving uploaded tools to temp file: %s", fullToolsFilename) + defer toolsFile.Close() + sha256hash := sha256.New() + var size int64 + if size, err = io.Copy(toolsFile, io.TeeReader(r, sha256hash)); err != nil { + return nil, false, fmt.Errorf("error processing file upload: %v", err) + } + if size == 0 { + return nil, false, fmt.Errorf("no tools uploaded") + } + + // TODO(wallyworld): check integrity of tools tarball. + + // Create a tools record and sync to storage. + uploadedTools := &tools.Tools{ + Version: toolsVersion, + Size: size, + SHA256: fmt.Sprintf("%x", sha256hash.Sum(nil)), + } + logger.Debugf("about to upload tools %+v to storage", uploadedTools) + return h.uploadToStorage(uploadedTools, tmpDir, toolsFilename, fakeSeries...) +} + +// uploadToStorage uploads the tools from the specified directory to environment storage. +func (h *toolsHandler) uploadToStorage(uploadedTools *tools.Tools, toolsDir, + toolsFilename string, fakeSeries ...string) (*tools.Tools, bool, error) { + + // SyncTools requires simplestreams metadata to find the tools to upload. + stor, err := filestorage.NewFileStorageWriter(toolsDir) + if err != nil { + return nil, false, fmt.Errorf("cannot create metadata storage: %v", err) + } + // Generate metadata for the fake series. The URL for each fake series + // record points to the same tools tarball. + allToolsMetadata := []*tools.Tools{uploadedTools} + for _, series := range fakeSeries { + vers := uploadedTools.Version + vers.Series = series + allToolsMetadata = append(allToolsMetadata, &tools.Tools{ + Version: vers, + URL: uploadedTools.URL, + Size: uploadedTools.Size, + SHA256: uploadedTools.SHA256, + }) + } + err = envtools.MergeAndWriteMetadata(stor, allToolsMetadata, false) + if err != nil { + return nil, false, fmt.Errorf("cannot get environment config: %v", err) + } + + // Create the environment so we can get the storage to which we upload the tools. + envConfig, err := h.state.EnvironConfig() + if err != nil { + return nil, false, fmt.Errorf("cannot get environment config: %v", err) + } + env, err := environs.New(envConfig) + if err != nil { + return nil, false, fmt.Errorf("cannot access environment: %v", err) + } + + // Now perform the upload. + builtTools := &sync.BuiltTools{ + Version: uploadedTools.Version, + Dir: toolsDir, + StorageName: toolsFilename, + Size: uploadedTools.Size, + Sha256Hash: uploadedTools.SHA256, + } + uploadedTools, err = sync.SyncBuiltTools(env.Storage(), builtTools, fakeSeries...) + if err != nil { + return nil, false, err + } + return uploadedTools, !envConfig.SSLHostnameVerification(), nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/tools_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/tools_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/tools_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/tools_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,197 @@ +// Copyright 2012, 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver_test + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "path" + + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs/tools" + ttesting "launchpad.net/juju-core/environs/tools/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" + coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/version" + "path/filepath" +) + +type toolsSuite struct { + authHttpSuite +} + +var _ = gc.Suite(&toolsSuite{}) + +func (s *toolsSuite) SetUpSuite(c *gc.C) { + s.JujuConnSuite.SetUpSuite(c) + s.archiveContentType = "application/x-tar-gz" +} + +func (s *toolsSuite) TestToolsUploadedSecurely(c *gc.C) { + _, info, err := s.APIConn.Environ.StateInfo() + c.Assert(err, gc.IsNil) + uri := "http://" + info.Addrs[0] + "/tools" + _, err = s.sendRequest(c, "", "", "PUT", uri, "", nil) + c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`) +} + +func (s *toolsSuite) TestRequiresAuth(c *gc.C) { + resp, err := s.sendRequest(c, "", "", "GET", s.toolsURI(c, ""), "", nil) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized") +} + +func (s *toolsSuite) TestRequiresPOST(c *gc.C) { + resp, err := s.authRequest(c, "PUT", s.toolsURI(c, ""), "", nil) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) +} + +func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) { + // Add a machine and try to login. + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, gc.IsNil) + err = machine.SetProvisioned("foo", "fake_nonce", nil) + c.Assert(err, gc.IsNil) + password, err := utils.RandomPassword() + c.Assert(err, gc.IsNil) + err = machine.SetPassword(password) + c.Assert(err, gc.IsNil) + + resp, err := s.sendRequest(c, machine.Tag(), password, "POST", s.toolsURI(c, ""), "", nil) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized") + + // Now try a user login. + resp, err = s.authRequest(c, "POST", s.toolsURI(c, ""), "", nil) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument") +} + +func (s *toolsSuite) TestUploadRequiresVersion(c *gc.C) { + resp, err := s.authRequest(c, "POST", s.toolsURI(c, ""), "", nil) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument") +} + +func (s *toolsSuite) TestUploadFailsWithNoTools(c *gc.C) { + // Create an empty file. + tempFile, err := ioutil.TempFile(c.MkDir(), "tools") + c.Assert(err, gc.IsNil) + + resp, err := s.uploadRequest(c, s.toolsURI(c, "?binaryVersion=1.18.0-quantal-amd64"), true, tempFile.Name()) + c.Assert(err, gc.IsNil) + s.assertErrorResponse(c, resp, http.StatusBadRequest, "no tools uploaded") +} + +func (s *toolsSuite) TestUploadFailsWithInvalidContentType(c *gc.C) { + // Create an empty file. + tempFile, err := ioutil.TempFile(c.MkDir(), "tools") + c.Assert(err, gc.IsNil) + + // Now try with the default Content-Type. + resp, err := s.uploadRequest(c, s.toolsURI(c, "?binaryVersion=1.18.0-quantal-amd64"), false, tempFile.Name()) + c.Assert(err, gc.IsNil) + s.assertErrorResponse( + c, resp, http.StatusBadRequest, "expected Content-Type: application/x-tar-gz, got: application/octet-stream") +} + +func (s *toolsSuite) TestUpload(c *gc.C) { + // Make some fake tools. + localStorage := c.MkDir() + vers := version.MustParseBinary("1.9.0-quantal-amd64") + versionStrings := []string{vers.String()} + expectedTools := ttesting.MakeToolsWithCheckSum(c, localStorage, "releases", versionStrings) + + // Now try uploading them. + toolsFile := tools.StorageName(vers) + resp, err := s.uploadRequest( + c, s.toolsURI(c, "?binaryVersion="+vers.String()), true, path.Join(localStorage, toolsFile)) + c.Assert(err, gc.IsNil) + + // Check the response. + stor := s.Conn.Environ.Storage() + toolsURL, err := stor.URL(tools.StorageName(vers)) + c.Assert(err, gc.IsNil) + expectedTools[0].URL = toolsURL + s.assertUploadResponse(c, resp, expectedTools[0]) + + // Check the contents. + r, err := stor.Get(tools.StorageName(vers)) + c.Assert(err, gc.IsNil) + uploadedData, err := ioutil.ReadAll(r) + c.Assert(err, gc.IsNil) + expectedData, err := ioutil.ReadFile(filepath.Join(localStorage, tools.StorageName(vers))) + c.Assert(err, gc.IsNil) + c.Assert(uploadedData, gc.DeepEquals, expectedData) +} + +func (s *toolsSuite) TestUploadFakeSeries(c *gc.C) { + // Make some fake tools. + localStorage := c.MkDir() + vers := version.MustParseBinary("1.9.0-quantal-amd64") + versionStrings := []string{vers.String()} + expectedTools := ttesting.MakeToolsWithCheckSum(c, localStorage, "releases", versionStrings) + + // Now try uploading them. + toolsFile := tools.StorageName(vers) + params := "?binaryVersion=" + vers.String() + "&series=precise,trusty" + resp, err := s.uploadRequest(c, s.toolsURI(c, params), true, path.Join(localStorage, toolsFile)) + c.Assert(err, gc.IsNil) + + // Check the response. + stor := s.Conn.Environ.Storage() + toolsURL, err := stor.URL(tools.StorageName(vers)) + c.Assert(err, gc.IsNil) + expectedTools[0].URL = toolsURL + s.assertUploadResponse(c, resp, expectedTools[0]) + + // Check the contents. + for _, series := range []string{"precise", "quantal", "trusty"} { + toolsVersion := vers + toolsVersion.Series = series + r, err := stor.Get(tools.StorageName(toolsVersion)) + c.Assert(err, gc.IsNil) + uploadedData, err := ioutil.ReadAll(r) + c.Assert(err, gc.IsNil) + expectedData, err := ioutil.ReadFile(filepath.Join(localStorage, tools.StorageName(vers))) + c.Assert(err, gc.IsNil) + c.Assert(uploadedData, gc.DeepEquals, expectedData) + } +} + +func (s *toolsSuite) toolsURI(c *gc.C, query string) string { + _, info, err := s.APIConn.Environ.StateInfo() + c.Assert(err, gc.IsNil) + return "https://" + info.Addrs[0] + "/tools" + query +} + +func (s *toolsSuite) assertUploadResponse(c *gc.C, resp *http.Response, agentTools *coretools.Tools) { + body := assertResponse(c, resp, http.StatusOK, "application/json") + toolsResult := jsonToolsResponse(c, body) + c.Check(toolsResult.Error, gc.IsNil) + c.Check(toolsResult.Tools, gc.DeepEquals, agentTools) +} + +func (s *toolsSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) { + body := assertResponse(c, resp, http.StatusOK, expContentType) + c.Check(string(body), gc.Equals, expBody) +} + +func (s *toolsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { + body := assertResponse(c, resp, expCode, "application/json") + err := jsonToolsResponse(c, body).Error + c.Assert(err, gc.NotNil) + c.Check(err, gc.ErrorMatches, expError) +} + +func jsonToolsResponse(c *gc.C, body []byte) (jsonResponse params.ToolsResult) { + err := json.Unmarshal(body, &jsonResponse) + c.Assert(err, gc.IsNil) + return +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -21,7 +22,6 @@ "launchpad.net/juju-core/state/apiserver/uniter" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) func Test(t *stdtesting.T) { @@ -1428,7 +1428,7 @@ instance.NewAddress("0.1.2.3"), }) c.Assert(err, gc.IsNil) - apiAddresses, err := s.State.APIAddresses() + apiAddresses, err := s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) result, err := s.uniter.APIAddresses() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,13 +4,13 @@ package upgrader import ( - agenttools "launchpad.net/juju-core/agent/tools" + "launchpad.net/juju-core/environs" + envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" "launchpad.net/juju-core/state/watcher" - "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) @@ -107,11 +107,10 @@ Results: make([]params.ToolsResult, len(args.Entities)), } for i, entity := range args.Entities { - err := common.ErrPerm + result.Results[i].Error = common.ServerError(common.ErrPerm) if u.authorizer.AuthOwner(entity.Tag) { - result.Results[i].Tools, err = u.getMachineTools(entity.Tag) + result.Results[i] = u.getMachineTools(entity.Tag) } - result.Results[i].Error = common.ServerError(err) } return result, nil } @@ -133,30 +132,55 @@ return u.st.Machine(id) } -func (u *UnitUpgraderAPI) getMachineTools(tag string) (*tools.Tools, error) { +func (u *UnitUpgraderAPI) getMachineTools(tag string) params.ToolsResult { + var result params.ToolsResult machine, err := u.getAssignedMachine(tag) if err != nil { - return nil, err + result.Error = common.ServerError(err) + return result } machineTools, err := machine.AgentTools() if err != nil { - return nil, err + result.Error = common.ServerError(err) + return result } // For older 1.16 upgrader workers, we need to supply a tools URL since the worker will attempt to // download the tools even though they already have been fetched by the machine agent. Newer upgrader - // workers do not have this problem. So to be compatible across all versions, we return the full tools - // metadata as recorded in the downloaded tools directory. - downloadedTools, err := agenttools.ReadTools(u.dataDir, machineTools.Version) + // workers do not have this problem. So to be compatible across all versions, we return the full + // tools metadata. + // TODO (wallyworld) - remove in 1.20, just return machineTools + cfg, err := u.st.EnvironConfig() + if err != nil { + result.Error = common.ServerError(err) + return result + } + // SSLHostnameVerification defaults to true, so we need to + // invert that, for backwards-compatibility (older versions + // will have DisableSSLHostnameVerification: false by default). + result.DisableSSLHostnameVerification = !cfg.SSLHostnameVerification() + env, err := environs.New(cfg) + if err != nil { + result.Error = common.ServerError(err) + return result + } + agentTools, err := envtools.FindExactTools( + env, machineTools.Version.Number, machineTools.Version.Series, machineTools.Version.Arch) if err != nil { - return nil, err + result.Error = common.ServerError(err) + return result } - return downloadedTools, nil + result.Tools = agentTools + return result } func (u *UnitUpgraderAPI) getMachineToolsVersion(tag string) (*version.Number, error) { - agentTools, err := u.getMachineTools(tag) + machine, err := u.getAssignedMachine(tag) + if err != nil { + return nil, err + } + machineTools, err := machine.AgentTools() if err != nil { return nil, err } - return &agentTools.Version.Number, nil + return &machineTools.Version.Number, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/unitupgrader_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,7 @@ package upgrader_test import ( - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" @@ -19,7 +15,6 @@ apiservertesting "launchpad.net/juju-core/state/apiserver/testing" "launchpad.net/juju-core/state/apiserver/upgrader" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) @@ -62,21 +57,6 @@ LoggedIn: true, UnitAgent: true, } - // Set up fake downloaded tools for the assigned machine. - fakeToolsPath := filepath.Join(s.DataDir(), "tools", version.Current.String()) - err = os.MkdirAll(fakeToolsPath, 0700) - c.Assert(err, gc.IsNil) - s.fakeTools = &tools.Tools{ - Version: version.Current, - URL: "fake-url", - Size: 1234, - SHA256: "checksum", - } - toolsMetadataData, err := json.Marshal(s.fakeTools) - c.Assert(err, gc.IsNil) - err = ioutil.WriteFile(filepath.Join(fakeToolsPath, "downloaded-tools.txt"), []byte(toolsMetadataData), 0644) - c.Assert(err, gc.IsNil) - s.upgrader, err = upgrader.NewUnitUpgraderAPI(s.State, s.resources, s.authorizer, s.DataDir()) c.Assert(err, gc.IsNil) } @@ -184,7 +164,8 @@ c.Check(results.Results, gc.HasLen, 1) c.Assert(results.Results[0].Error, gc.IsNil) agentTools := results.Results[0].Tools - c.Check(agentTools, gc.DeepEquals, s.fakeTools) + c.Check(agentTools.Version.Number, gc.DeepEquals, version.Current.Number) + c.Assert(agentTools.URL, gc.NotNil) } assertTools() } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/upgrader_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/upgrader_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/upgrader/upgrader_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/upgrader/upgrader_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package upgrader_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" envtesting "launchpad.net/juju-core/environs/testing" @@ -15,7 +16,6 @@ apiservertesting "launchpad.net/juju-core/state/apiserver/testing" "launchpad.net/juju-core/state/apiserver/upgrader" statetesting "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager_test + +import ( + stdtesting "testing" + + "launchpad.net/juju-core/testing" +) + +func TestAll(t *stdtesting.T) { + testing.MgoTestPackage(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,105 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager + +import ( + "fmt" + + "github.com/loggo/loggo" + + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/state/apiserver/common" +) + +var logger = loggo.GetLogger("juju.state.apiserver.usermanager") + +// UserManager defines the methods on the usermanager API end point. +type UserManager interface { + AddUser(arg params.EntityPasswords) (params.ErrorResults, error) + RemoveUser(arg params.Entities) (params.ErrorResults, error) +} + +// UserManagerAPI implements the user manager interface and is the concrete +// implementation of the api end point. +type UserManagerAPI struct { + state *state.State + authorizer common.Authorizer + getCanWrite common.GetAuthFunc +} + +var _ UserManager = (*UserManagerAPI)(nil) + +func NewUserManagerAPI( + st *state.State, + authorizer common.Authorizer, +) (*UserManagerAPI, error) { + if !authorizer.AuthClient() { + return nil, common.ErrPerm + } + + // TODO(mattyw) - replace stub with real canWrite function + getCanWrite := common.AuthAlways(true) + return &UserManagerAPI{ + state: st, + authorizer: authorizer, + getCanWrite: getCanWrite}, + nil +} + +func (api *UserManagerAPI) AddUser(args params.EntityPasswords) (params.ErrorResults, error) { + result := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Changes)), + } + if len(args.Changes) == 0 { + return result, nil + } + canWrite, err := api.getCanWrite() + if err != nil { + result.Results[0].Error = common.ServerError(err) + return result, err + } + for i, arg := range args.Changes { + if !canWrite(arg.Tag) { + result.Results[0].Error = common.ServerError(common.ErrPerm) + continue + } + _, err := api.state.AddUser(arg.Tag, arg.Password) + if err != nil { + result.Results[i].Error = common.ServerError(fmt.Errorf("Failed to create user: %s", err)) + continue + } + } + return result, nil +} + +func (api *UserManagerAPI) RemoveUser(args params.Entities) (params.ErrorResults, error) { + result := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Entities)), + } + if len(args.Entities) == 0 { + return result, nil + } + canWrite, err := api.getCanWrite() + if err != nil { + return result, err + } + for i, arg := range args.Entities { + if !canWrite(arg.Tag) { + result.Results[i].Error = common.ServerError(common.ErrPerm) + continue + } + user, err := api.state.User(arg.Tag) + if err != nil { + result.Results[i].Error = common.ServerError(common.ErrPerm) + continue + } + err = user.Deactivate() + if err != nil { + result.Results[i].Error = common.ServerError(fmt.Errorf("Failed to remove user: %s", err)) + continue + } + } + return result, nil +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/usermanager/usermanager_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,113 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package usermanager_test + +import ( + gc "launchpad.net/gocheck" + + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state/api/params" + apiservertesting "launchpad.net/juju-core/state/apiserver/testing" + "launchpad.net/juju-core/state/apiserver/usermanager" +) + +type userManagerSuite struct { + jujutesting.JujuConnSuite + + usermanager *usermanager.UserManagerAPI + authorizer apiservertesting.FakeAuthorizer +} + +var _ = gc.Suite(&userManagerSuite{}) + +func (s *userManagerSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + + s.authorizer = apiservertesting.FakeAuthorizer{ + Tag: "user-admin", + LoggedIn: true, + Client: true, + } + + var err error + s.usermanager, err = usermanager.NewUserManagerAPI(s.State, s.authorizer) + c.Assert(err, gc.IsNil) +} + +func (s *userManagerSuite) TestNewUserManagerAPIRefusesNonClient(c *gc.C) { + anAuthoriser := s.authorizer + anAuthoriser.Client = false + endPoint, err := usermanager.NewUserManagerAPI(s.State, anAuthoriser) + c.Assert(endPoint, gc.IsNil) + c.Assert(err, gc.ErrorMatches, "permission denied") +} + +func (s *userManagerSuite) TestAddUser(c *gc.C) { + arg := params.EntityPassword{ + Tag: "foobar", + Password: "password", + } + + args := params.EntityPasswords{Changes: []params.EntityPassword{arg}} + + result, err := s.usermanager.AddUser(args) + // Check that the call is succesful + c.Assert(err, gc.IsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{Results: []params.ErrorResult{params.ErrorResult{Error: nil}}}) + // Check that the call results in a new user being created + user, err := s.State.User("foobar") + c.Assert(err, gc.IsNil) + c.Assert(user, gc.NotNil) +} + +func (s *userManagerSuite) TestRemoveUser(c *gc.C) { + arg := params.EntityPassword{ + Tag: "foobar", + Password: "password", + } + removeArg := params.Entity{ + Tag: "foobar", + } + args := params.EntityPasswords{Changes: []params.EntityPassword{arg}} + removeArgs := params.Entities{Entities: []params.Entity{removeArg}} + _, err := s.usermanager.AddUser(args) + c.Assert(err, gc.IsNil) + user, err := s.State.User("foobar") + c.Assert(user.IsDeactivated(), gc.Equals, false) // The user should be active + + result, err := s.usermanager.RemoveUser(removeArgs) + c.Assert(err, gc.IsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{Results: []params.ErrorResult{params.ErrorResult{Error: nil}}}) + user, err = s.State.User("foobar") + c.Assert(err, gc.IsNil) + // Removal makes the user in active + c.Assert(user.IsDeactivated(), gc.Equals, true) + c.Assert(user.PasswordValid(arg.Password), gc.Equals, false) +} + +// Since removing a user just deacitvates them you cannot add a user +// that has been previously been removed +// TODO(mattyw) 2014-03-07 bug #1288745 +func (s *userManagerSuite) TestCannotAddRemoveAdd(c *gc.C) { + arg := params.EntityPassword{ + Tag: "addremove", + Password: "password", + } + removeArg := params.Entity{ + Tag: "foobar", + } + args := params.EntityPasswords{Changes: []params.EntityPassword{arg}} + removeArgs := params.Entities{Entities: []params.Entity{removeArg}} + _, err := s.usermanager.AddUser(args) + c.Assert(err, gc.IsNil) + + _, err = s.usermanager.RemoveUser(removeArgs) + c.Assert(err, gc.IsNil) + _, err = s.State.User("addremove") + result, err := s.usermanager.AddUser(args) + expectedError := params.Error{Code: "", Message: "Failed to create user: user already exists"} + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + params.ErrorResult{&expectedError}}}) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/assign_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/assign_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/assign_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/assign_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,12 +9,12 @@ "strconv" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" ) type AssignSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/charm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/charm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/charm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/charm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,13 +7,13 @@ "bytes" "net/url" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type CharmSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/cleanup.go juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/cleanup.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup.go 2014-03-20 12:52:38.000000000 +0000 @@ -85,7 +85,7 @@ // Documents marked for cleanup are not otherwise referenced in the // system, and will not be under watch, and are therefore safe to // delete directly. - sel := D{{"_id", D{{"$regex", "^" + prefix}}}} + sel := bson.D{{"_id", bson.D{{"$regex", "^" + prefix}}}} if count, err := st.settings.Find(sel).Count(); err != nil { return fmt.Errorf("cannot detect cleanup targets: %v", err) } else if count != 0 { @@ -103,7 +103,7 @@ // services added to it. But we do have to remove the services themselves // via individual transactions, because they could be in any state at all. service := &Service{st: st} - sel := D{{"life", Alive}} + sel := bson.D{{"life", Alive}} iter := st.services.Find(sel).Iter() for iter.Next(&service.doc) { if err := service.Destroy(); err != nil { @@ -123,7 +123,7 @@ // to it. But we do have to remove the units themselves via individual // transactions, because they could be in any state at all. unit := &Unit{st: st} - sel := D{{"_id", D{{"$regex", "^" + prefix}}}, {"life", Alive}} + sel := bson.D{{"_id", bson.D{{"$regex", "^" + prefix}}}, {"life", Alive}} iter := st.units.Find(sel).Iter() for iter.Next(&unit.doc) { if err := unit.Destroy(); err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/cleanup_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/cleanup_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -3,13 +3,13 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" ) type CleanupSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/compat_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/compat_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/compat_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/compat_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package state import ( + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" gc "launchpad.net/gocheck" @@ -54,7 +55,7 @@ ops := []txn.Op{{ C: s.state.environments.Name, Id: s.env.doc.UUID, - Update: D{{"$unset", D{{"life", nil}}}}, + Update: bson.D{{"$unset", bson.D{{"life", nil}}}}, }} err := s.state.runTransaction(ops) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/configvalidator_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/configvalidator_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/configvalidator_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/configvalidator_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,114 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + gc "launchpad.net/gocheck" + "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/state" + coretesting "launchpad.net/juju-core/testing" +) + +type ConfigValidatorSuite struct { + ConnSuite + configValidator mockConfigValidator +} + +var _ = gc.Suite(&ConfigValidatorSuite{}) + +type mockConfigValidator struct { + validateError error + validateCfg *config.Config + validateOld *config.Config + validateValid *config.Config +} + +// To test UpdateEnvironConfig updates state, Validate returns a config +// different to both input configs +func mockValidCfg() (valid *config.Config, err error) { + cfg, err := config.New(config.UseDefaults, coretesting.FakeConfig()) + if err != nil { + return nil, err + } + valid, err = cfg.Apply(map[string]interface{}{ + "arbitrary-key": "cptn-marvel", + }) + if err != nil { + return nil, err + } + return valid, nil +} + +func (p *mockConfigValidator) Validate(cfg, old *config.Config) (valid *config.Config, err error) { + p.validateCfg = cfg + p.validateOld = old + p.validateValid, p.validateError = mockValidCfg() + return p.validateValid, p.validateError +} + +func (s *ConfigValidatorSuite) SetUpTest(c *gc.C) { + s.ConnSuite.SetUpTest(c) + s.configValidator = mockConfigValidator{} + s.policy.getConfigValidator = func(string) (state.ConfigValidator, error) { + return &s.configValidator, nil + } +} + +func (s *ConfigValidatorSuite) updateEnvironConfig(c *gc.C) error { + updateAttrs := map[string]interface{}{ + "authorized-keys": "different-keys", + "arbitrary-key": "shazam!", + } + return s.State.UpdateEnvironConfig(updateAttrs, nil, nil) +} + +func (s *ConfigValidatorSuite) TestConfigValidate(c *gc.C) { + err := s.updateEnvironConfig(c) + c.Assert(err, gc.IsNil) +} + +func (s *ConfigValidatorSuite) TestUpdateEnvironConfigFailsOnConfigValidateError(c *gc.C) { + var configValidatorErr error + s.policy.getConfigValidator = func(string) (state.ConfigValidator, error) { + configValidatorErr = errors.NotFoundf("") + return &s.configValidator, configValidatorErr + } + + err := s.updateEnvironConfig(c) + c.Assert(err, gc.ErrorMatches, " not found") +} + +func (s *ConfigValidatorSuite) TestUpdateEnvironConfigUpdatesState(c *gc.C) { + s.updateEnvironConfig(c) + stateCfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + newValidCfg, err := mockValidCfg() + c.Assert(err, gc.IsNil) + c.Assert(stateCfg.AllAttrs()["arbitrary-key"], gc.Equals, newValidCfg.AllAttrs()["arbitrary-key"]) +} + +func (s *ConfigValidatorSuite) TestConfigValidateUnimplemented(c *gc.C) { + var configValidatorErr error + s.policy.getConfigValidator = func(string) (state.ConfigValidator, error) { + return nil, configValidatorErr + } + + err := s.updateEnvironConfig(c) + c.Assert(err, gc.ErrorMatches, "policy returned nil configValidator without an error") + configValidatorErr = errors.NewNotImplementedError("Validator") + err = s.updateEnvironConfig(c) + c.Assert(err, gc.IsNil) +} + +func (s *ConfigValidatorSuite) TestConfigValidateNoPolicy(c *gc.C) { + s.policy.getConfigValidator = func(providerType string) (state.ConfigValidator, error) { + c.Errorf("should not have been invoked") + return nil, nil + } + + state.SetPolicy(s.State, nil) + err := s.updateEnvironConfig(c) + c.Assert(err, gc.IsNil) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/conn_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/conn_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/conn_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/conn_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -59,11 +59,14 @@ cs.services = cs.MgoSuite.Session.DB("juju").C("services") cs.units = cs.MgoSuite.Session.DB("juju").C("units") cs.stateServers = cs.MgoSuite.Session.DB("juju").C("stateServers") - cs.State.AddUser("admin", "pass") + cs.State.AddUser(state.AdminUser, "pass") } func (cs *ConnSuite) TearDownTest(c *gc.C) { - cs.State.Close() + if cs.State != nil { + // If setup fails, we don't have a State yet + cs.State.Close() + } cs.MgoSuite.TearDownTest(c) cs.LoggingSuite.TearDownTest(c) } @@ -94,7 +97,8 @@ } type mockPolicy struct { - getPrechecker func(*config.Config) (state.Prechecker, error) + getPrechecker func(*config.Config) (state.Prechecker, error) + getConfigValidator func(string) (state.ConfigValidator, error) } func (p *mockPolicy) Prechecker(cfg *config.Config) (state.Prechecker, error) { @@ -103,3 +107,10 @@ } return nil, errors.NewNotImplementedError("Prechecker") } + +func (p *mockPolicy) ConfigValidator(providerType string) (state.ConfigValidator, error) { + if p.getConfigValidator != nil { + return p.getConfigValidator(providerType) + } + return nil, errors.NewNotImplementedError("ConfigValidator") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/constraints.go juju-core-1.17.6/src/launchpad.net/juju-core/state/constraints.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/constraints.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/constraints.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/constraints" @@ -63,7 +64,7 @@ C: st.constraints.Name, Id: id, Assert: txn.DocExists, - Update: D{{"$set", newConstraintsDoc(cons)}}, + Update: bson.D{{"$set", newConstraintsDoc(cons)}}, } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/container.go juju-core-1.17.6/src/launchpad.net/juju-core/state/container.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/container.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/container.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "strings" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/instance" @@ -23,7 +24,7 @@ C: st.containerRefs.Name, Id: parentId, Assert: txn.DocExists, - Update: D{{"$addToSet", D{{"children", childId}}}}, + Update: bson.D{{"$addToSet", bson.D{{"children", childId}}}}, } } @@ -57,7 +58,7 @@ C: st.containerRefs.Name, Id: parentId, Assert: txn.DocExists, - Update: D{{"$pull", D{{"children", machineId}}}}, + Update: bson.D{{"$pull", bson.D{{"children", machineId}}}}, } return []txn.Op{removeRefOp, removeParentRefOp} } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/environ.go juju-core-1.17.6/src/launchpad.net/juju-core/state/environ.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/environ.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/environ.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,6 +5,7 @@ import ( "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/errors" @@ -102,7 +103,7 @@ ops := []txn.Op{{ C: e.st.environments.Name, Id: e.doc.UUID, - Update: D{{"$set", D{{"life", Dying}}}}, + Update: bson.D{{"$set", bson.D{{"life", Dying}}}}, Assert: isEnvAliveDoc, }, e.st.newCleanupOp("services", "")} err := e.st.runTransaction(ops) @@ -144,6 +145,6 @@ // Environment documents from versions of Juju prior to 1.17 // do not have the life field; if it does not exist, it should // be considered to have the value Alive. -var isEnvAliveDoc = D{ - {"life", D{{"$in", []interface{}{Alive, nil}}}}, +var isEnvAliveDoc = bson.D{ + {"life", bson.D{{"$in", []interface{}{Alive, nil}}}}, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,7 @@ "path/filepath" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" gc "launchpad.net/gocheck" @@ -189,7 +190,7 @@ C: m.st.instanceData.Name, Id: m.doc.Id, Assert: txn.DocExists, - Update: D{{"$set", D{{"instanceid", ""}}}}, + Update: bson.D{{"$set", bson.D{{"instanceid", ""}}}}, }, } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/initialize_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/initialize_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/initialize_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/initialize_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,14 +4,13 @@ package state_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs/config" - "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) @@ -36,6 +35,9 @@ func (s *InitializeSuite) SetUpTest(c *gc.C) { s.LoggingSuite.SetUpTest(c) s.MgoSuite.SetUpTest(c) +} + +func (s *InitializeSuite) openState(c *gc.C) { var err error s.State, err = state.Open(state.TestingStateInfo(), state.TestingDialOpts(), state.Policy(nil)) c.Assert(err, gc.IsNil) @@ -48,15 +50,6 @@ } func (s *InitializeSuite) TestInitialize(c *gc.C) { - _, err := s.State.EnvironConfig() - c.Assert(err, jc.Satisfies, errors.IsNotFoundError) - _, err = s.State.FindEntity("environment-foo") - // TODO(axw) 2013-12-04 #1257587 - // remove backwards compatibility for environment-tag; see state.go - c.Assert(err, jc.Satisfies, errors.IsNotFoundError) - _, err = s.State.EnvironConstraints() - c.Assert(err, jc.Satisfies, errors.IsNotFoundError) - cfg := testing.EnvironConfig(c) initial := cfg.AllAttrs() st, err := state.Initialize(state.TestingStateInfo(), cfg, state.TestingDialOpts(), state.Policy(nil)) @@ -65,6 +58,8 @@ err = st.Close() c.Assert(err, gc.IsNil) + s.openState(c) + cfg, err = s.State.EnvironConfig() c.Assert(err, gc.IsNil) c.Assert(cfg.AllAttrs(), gc.DeepEquals, initial) @@ -80,6 +75,14 @@ cons, err := s.State.EnvironConstraints() c.Assert(err, gc.IsNil) c.Assert(&cons, jc.Satisfies, constraints.IsEmpty) + + addrs, err := s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(addrs, gc.HasLen, 0) + + info, err := s.State.StateServerInfo() + c.Assert(err, gc.IsNil) + c.Assert(info, jc.DeepEquals, &state.StateServerInfo{}) } func (s *InitializeSuite) TestDoubleInitializeConfig(c *gc.C) { @@ -98,6 +101,7 @@ c.Assert(st, gc.NotNil) st.Close() + s.openState(c) cfg, err = s.State.EnvironConfig() c.Assert(err, gc.IsNil) c.Assert(cfg.AllAttrs(), gc.DeepEquals, initial) @@ -106,15 +110,18 @@ func (s *InitializeSuite) TestEnvironConfigWithAdminSecret(c *gc.C) { // admin-secret blocks Initialize. good := testing.EnvironConfig(c) - bad, err := good.Apply(map[string]interface{}{"admin-secret": "foo"}) + badUpdateAttrs := map[string]interface{}{"admin-secret": "foo"} + bad, err := good.Apply(badUpdateAttrs) _, err = state.Initialize(state.TestingStateInfo(), bad, state.TestingDialOpts(), state.Policy(nil)) c.Assert(err, gc.ErrorMatches, "admin-secret should never be written to the state") - // admin-secret blocks SetEnvironConfig. + // admin-secret blocks UpdateEnvironConfig. st := state.TestingInitialize(c, good, state.Policy(nil)) st.Close() - err = s.State.SetEnvironConfig(bad, good) + + s.openState(c) + err = s.State.UpdateEnvironConfig(badUpdateAttrs, nil, nil) c.Assert(err, gc.ErrorMatches, "admin-secret should never be written to the state") // EnvironConfig remains inviolate. @@ -134,10 +141,11 @@ _, err = state.Initialize(state.TestingStateInfo(), bad, state.TestingDialOpts(), state.Policy(nil)) c.Assert(err, gc.ErrorMatches, "agent-version must always be set in state") - // Bad agent-version blocks SetEnvironConfig. st := state.TestingInitialize(c, good, state.Policy(nil)) st.Close() - err = s.State.SetEnvironConfig(bad, good) + + s.openState(c) + err = s.State.UpdateEnvironConfig(map[string]interface{}{}, []string{"agent-version"}, nil) c.Assert(err, gc.ErrorMatches, "agent-version must always be set in state") // EnvironConfig remains inviolate. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/life.go juju-core-1.17.6/src/launchpad.net/juju-core/state/life.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/life.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/life.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,6 +5,7 @@ import ( "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "launchpad.net/juju-core/state/api/params" ) @@ -30,9 +31,9 @@ return string(lifeStrings[l]) } -var isAliveDoc = D{{"life", Alive}} -var isDeadDoc = D{{"life", Dead}} -var notDeadDoc = D{{"life", D{{"$ne", Dead}}}} +var isAliveDoc = bson.D{{"life", Alive}} +var isDeadDoc = bson.D{{"life", Dead}} +var notDeadDoc = bson.D{{"life", bson.D{{"$ne", Dead}}}} // Living describes state entities with a lifecycle. type Living interface { @@ -50,11 +51,11 @@ } func isAlive(coll *mgo.Collection, id interface{}) (bool, error) { - n, err := coll.Find(D{{"_id", id}, {"life", Alive}}).Count() + n, err := coll.Find(bson.D{{"_id", id}, {"life", Alive}}).Count() return n == 1, err } func isNotDead(coll *mgo.Collection, id interface{}) (bool, error) { - n, err := coll.Find(D{{"_id", id}, {"life", D{{"$ne", Dead}}}}).Count() + n, err := coll.Find(bson.D{{"_id", id}, {"life", bson.D{{"$ne", Dead}}}}).Count() return n == 1, err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/life_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/life_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/life_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/life_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package state_test import ( + "labix.org/v2/mgo/bson" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state" @@ -117,14 +118,14 @@ collName, id := lfix.id() coll := s.MgoSuite.Session.DB("juju").C(collName) - err := coll.UpdateId(id, D{{"$set", D{ + err := coll.UpdateId(id, bson.D{{"$set", bson.D{ {"life", cached}, }}}) c.Assert(err, gc.IsNil) err = living.Refresh() c.Assert(err, gc.IsNil) - err = coll.UpdateId(id, D{{"$set", D{ + err = coll.UpdateId(id, bson.D{{"$set", bson.D{ {"life", dbinitial}, }}}) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/machine.go juju-core-1.17.6/src/launchpad.net/juju-core/state/machine.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/machine.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/machine.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "time" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/constraints" @@ -225,7 +226,7 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{{"$set", D{{"hasvote", hasVote}}}}, + Update: bson.D{{"$set", bson.D{{"hasvote", hasVote}}}}, }} if err := m.st.runTransaction(ops); err != nil { return fmt.Errorf("cannot set HasVote of machine %v: %v", m, onAbort(err, errDead)) @@ -292,7 +293,7 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{{"$set", D{{"tools", tools}}}}, + Update: bson.D{{"$set", bson.D{{"tools", tools}}}}, }} if err := m.st.runTransaction(ops); err != nil { return onAbort(err, errDead) @@ -324,7 +325,7 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{{"$set", D{{"passwordhash", passwordHash}}}}, + Update: bson.D{{"$set", bson.D{{"passwordhash", passwordHash}}}}, }} if err := m.st.runTransaction(ops); err != nil { return fmt.Errorf("cannot set password of machine %v: %v", m, onAbort(err, errDead)) @@ -377,7 +378,7 @@ ops := []txn.Op{{ C: m.st.machines.Name, Id: m.doc.Id, - Assert: D{{"jobs", D{{"$nin", []MachineJob{JobManageEnviron}}}}}, + Assert: bson.D{{"jobs", bson.D{{"$nin", []MachineJob{JobManageEnviron}}}}}, }, m.st.newCleanupOp("machine", m.doc.Id)} if err := m.st.runTransaction(ops); err != txn.ErrAborted { return err @@ -477,15 +478,15 @@ op := txn.Op{ C: m.st.machines.Name, Id: m.doc.Id, - Update: D{{"$set", D{{"life", life}}}}, + Update: bson.D{{"$set", bson.D{{"life", life}}}}, } - advanceAsserts := D{ - {"jobs", D{{"$nin", []MachineJob{JobManageEnviron}}}}, - {"$or", []D{ - {{"principals", D{{"$size", 0}}}}, - {{"principals", D{{"$exists", false}}}}, + advanceAsserts := bson.D{ + {"jobs", bson.D{{"$nin", []MachineJob{JobManageEnviron}}}}, + {"$or", []bson.D{ + {{"principals", bson.D{{"$size", 0}}}}, + {{"principals", bson.D{{"$exists", false}}}}, }}, - {"hasvote", D{{"$ne", true}}}, + {"hasvote", bson.D{{"$ne", true}}}, } // 3 attempts: one with original data, one with refreshed data, and a final // one intended to determine the cause of failure of the preceding attempt. @@ -677,13 +678,13 @@ // SCHEMACHANGE - we can't do this yet until the schema is updated // so just do a txn.DocExists for now. - // provisioned := D{{"instanceid", D{{"$ne", ""}}}} + // provisioned := bson.D{{"instanceid", bson.D{{"$ne", ""}}}} ops := []txn.Op{ { C: m.st.instanceData.Name, Id: m.doc.Id, Assert: txn.DocExists, - Update: D{{"$set", D{{"status", status}}}}, + Update: bson.D{{"$set", bson.D{{"status", status}}}}, }, } @@ -699,14 +700,14 @@ func (m *Machine) Units() (units []*Unit, err error) { defer utils.ErrorContextf(&err, "cannot get units assigned to machine %v", m) pudocs := []unitDoc{} - err = m.st.units.Find(D{{"machineid", m.doc.Id}}).All(&pudocs) + err = m.st.units.Find(bson.D{{"machineid", m.doc.Id}}).All(&pudocs) if err != nil { return nil, err } for _, pudoc := range pudocs { units = append(units, newUnit(m.st, &pudoc)) docs := []unitDoc{} - err = m.st.units.Find(D{{"principal", pudoc.Name}}).All(&docs) + err = m.st.units.Find(bson.D{{"principal", pudoc.Name}}).All(&docs) if err != nil { return nil, err } @@ -747,13 +748,13 @@ } // SCHEMACHANGE // TODO(wallyworld) - do not check instanceId on machineDoc after schema is upgraded - notSetYet := D{{"instanceid", ""}, {"nonce", ""}} + notSetYet := bson.D{{"instanceid", ""}, {"nonce", ""}} ops := []txn.Op{ { C: m.st.machines.Name, Id: m.doc.Id, Assert: append(isAliveDoc, notSetYet...), - Update: D{{"$set", D{{"instanceid", id}, {"nonce", nonce}}}}, + Update: bson.D{{"$set", bson.D{{"instanceid", id}, {"nonce", nonce}}}}, }, { C: m.st.instanceData.Name, Id: m.doc.Id, @@ -826,7 +827,7 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{{"$set", D{{"addresses", stateAddresses}}}}, + Update: bson.D{{"$set", bson.D{{"addresses", stateAddresses}}}}, }, } @@ -855,7 +856,7 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{{"$set", D{{"machineaddresses", stateAddresses}}}}, + Update: bson.D{{"$set", bson.D{{"machineaddresses", stateAddresses}}}}, }, } @@ -887,7 +888,7 @@ // is already provisioned. func (m *Machine) SetConstraints(cons constraints.Value) (err error) { defer utils.ErrorContextf(&err, "cannot set constraints") - notSetYet := D{{"nonce", ""}} + notSetYet := bson.D{{"nonce", ""}} ops := []txn.Op{ { C: m.st.machines.Name, @@ -1006,8 +1007,8 @@ C: m.st.machines.Name, Id: m.doc.Id, Assert: notDeadDoc, - Update: D{ - {"$set", D{ + Update: bson.D{ + {"$set", bson.D{ {"supportedcontainers", supportedContainers}, {"supportedcontainersknown", true}, }}}, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/machine_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/machine_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/machine_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/machine_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,8 +4,10 @@ package state_test import ( + "labix.org/v2/mgo/bson" "sort" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -15,7 +17,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/version" ) @@ -79,9 +80,8 @@ manual, err := s.machine0.IsManual() c.Assert(err, gc.IsNil) c.Assert(manual, jc.IsFalse) - newcfg, err := cfg.Apply(map[string]interface{}{"type": "null"}) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(newcfg, cfg) + attrs := map[string]interface{}{"type": "null"} + err = s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) manual, err = s.machine0.IsManual() c.Assert(err, gc.IsNil) @@ -352,8 +352,8 @@ machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, gc.IsNil) err = s.machines.Update( - D{{"_id", machine.Id()}}, - D{{"$set", D{{"instanceid", "spaceship/0"}}}}, + bson.D{{"_id", machine.Id()}}, + bson.D{{"$set", bson.D{{"instanceid", "spaceship/0"}}}}, ) c.Assert(err, gc.IsNil) @@ -368,8 +368,8 @@ machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, gc.IsNil) err = s.machines.Update( - D{{"_id", machine.Id()}}, - D{{"$set", D{{"instanceid", D{{"foo", "bar"}}}}}}, + bson.D{{"_id", machine.Id()}}, + bson.D{{"$set", bson.D{{"instanceid", bson.D{{"foo", "bar"}}}}}}, ) c.Assert(err, gc.IsNil) @@ -390,8 +390,8 @@ machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, gc.IsNil) err = s.machines.Update( - D{{"_id", machine.Id()}}, - D{{"$set", D{{"instanceid", ""}}}}, + bson.D{{"_id", machine.Id()}}, + bson.D{{"$set", bson.D{{"instanceid", ""}}}}, ) c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/megawatcher_internal_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher_internal_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/megawatcher_internal_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher_internal_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -48,7 +48,7 @@ s.LoggingSuite.SetUpTest(c) s.MgoSuite.SetUpTest(c) s.State = TestingInitialize(c, nil, Policy(nil)) - s.State.AddUser("admin", "pass") + s.State.AddUser(AdminUser, "pass") } func (s *storeManagerStateSuite) TearDownTest(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/minimumunits.go juju-core-1.17.6/src/launchpad.net/juju-core/state/minimumunits.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/minimumunits.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/minimumunits.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "errors" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/utils" @@ -73,7 +74,7 @@ C: state.services.Name, Id: serviceName, Assert: isAliveDoc, - Update: D{{"$set", D{{"minunits", minUnits}}}}, + Update: bson.D{{"$set", bson.D{{"minunits", minUnits}}}}, }} if service.doc.MinUnits == 0 { return append(ops, txn.Op{ @@ -104,7 +105,7 @@ return txn.Op{ C: st.minUnits.Name, Id: serviceName, - Update: D{{"$inc", D{{"revno", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"revno", 1}}}}, } } @@ -180,7 +181,7 @@ // aliveUnitsCount returns the number a alive units for the service. func aliveUnitsCount(service *Service) (int, error) { - query := D{{"service", service.doc.Name}, {"life", Alive}} + query := bson.D{{"service", service.doc.Name}, {"life", Alive}} return service.st.units.Find(query).Count() } @@ -188,6 +189,6 @@ // service in MongoDB and the name for the new unit. The resulting transaction // will be aborted if the service document changes when running the operations. func ensureMinUnitsOps(service *Service) (string, []txn.Op, error) { - asserts := D{{"txn-revno", service.doc.TxnRevno}} + asserts := bson.D{{"txn-revno", service.doc.TxnRevno}} return service.addUnitOps("", asserts) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/open.go juju-core-1.17.6/src/launchpad.net/juju-core/state/open.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/open.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/open.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,6 +12,7 @@ "time" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/cert" @@ -159,8 +160,12 @@ createEnvironmentOp(st, cfg.Name(), uuid.String()), { C: st.stateServers.Name, - Id: "", + Id: environGlobalKey, Insert: &stateServersDoc{}, + }, { + C: st.stateServers.Name, + Id: apiHostPortsKey, + Insert: &apiHostPortsDoc{}, }, } if err := st.runTransaction(ops); err == txn.ErrAborted { @@ -233,8 +238,8 @@ return nil, maybeUnauthorized(err, fmt.Sprintf("cannot log in to presence database as %q", info.Tag)) } } else if info.Password != "" { - admin := session.DB("admin") - if err := admin.Login("admin", info.Password); err != nil { + admin := session.DB(AdminUser) + if err := admin.Login(AdminUser, info.Password); err != nil { return nil, maybeUnauthorized(err, "cannot log in to admin database") } } @@ -288,6 +293,9 @@ if err := st.createStateServersDoc(); err != nil { return nil, fmt.Errorf("cannot create state servers document: %v", err) } + if err := st.createAPIAddressesDoc(); err != nil { + return nil, fmt.Errorf("cannot create API addresses document: %v", err) + } return st, nil } @@ -312,7 +320,7 @@ // we're concerned about, there is only ever one state connection // (from the single bootstrap machine). var machineDocs []machineDoc - err := st.machines.Find(D{{"jobs", JobManageEnviron}}).All(&machineDocs) + err := st.machines.Find(bson.D{{"jobs", JobManageEnviron}}).All(&machineDocs) if err != nil { return err } @@ -330,7 +338,7 @@ ops := []txn.Op{{ C: st.stateServers.Name, Id: environGlobalKey, - Update: D{{"$set", D{ + Update: bson.D{{"$set", bson.D{ {"machineids", doc.MachineIds}, {"votingmachineids", doc.VotingMachineIds}, }}}, @@ -343,6 +351,21 @@ return st.runTransaction(ops) } +// createAPIAddressesDoc creates the API addresses document +// if it does not already exist. This is necessary to cope with +// legacy environments that have not created the document +// at initialization time. +func (st *State) createAPIAddressesDoc() error { + var doc apiHostPortsDoc + ops := []txn.Op{{ + C: st.stateServers.Name, + Id: apiHostPortsKey, + Assert: txn.DocMissing, + Insert: &doc, + }} + return onAbort(st.runTransaction(ops), nil) +} + // CACert returns the certificate used to validate the state connection. func (st *State) CACert() (cert []byte) { return append(cert, st.info.CACert...) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/policy.go juju-core-1.17.6/src/launchpad.net/juju-core/state/policy.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/policy.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/policy.go 2014-03-20 12:52:38.000000000 +0000 @@ -24,6 +24,10 @@ // Prechecker takes a *config.Config and returns // a (possibly nil) Prechecker or an error. Prechecker(*config.Config) (Prechecker, error) + + // ConfigValidator takes a string (environ type) and returns + // a (possibly nil) ConfigValidator or an error. + ConfigValidator(string) (ConfigValidator, error) } // Prechecker is a policy interface that is provided to State @@ -40,6 +44,12 @@ PrecheckInstance(series string, cons constraints.Value) error } +// ConfigValidator is a policy interface that is provided to State +// to check validity of new configuration attributes before applying them to state. +type ConfigValidator interface { + Validate(cfg, old *config.Config) (valid *config.Config, err error) +} + // precheckInstance calls the state's assigned policy, if non-nil, to obtain // a Prechecker, and calls PrecheckInstance if a non-nil Prechecker is returned. func (st *State) precheckInstance(series string, cons constraints.Value) error { @@ -61,3 +71,21 @@ } return prechecker.PrecheckInstance(series, cons) } + +// validate calls the state's assigned policy, if non-nil, to obtain +// a Validator, and calls validate if a non-nil Validator is returned. +func (st *State) validate(cfg, old *config.Config) (valid *config.Config, err error) { + if st.policy == nil { + return cfg, nil + } + configValidator, err := st.policy.ConfigValidator(cfg.Type()) + if errors.IsNotImplementedError(err) { + return cfg, nil + } else if err != nil { + return nil, err + } + if configValidator == nil { + return nil, fmt.Errorf("policy returned nil configValidator without an error") + } + return configValidator.Validate(cfg, old) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/relation.go juju-core-1.17.6/src/launchpad.net/juju-core/state/relation.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/relation.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/relation.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,6 +11,7 @@ "strings" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/charm" @@ -152,8 +153,8 @@ return []txn.Op{{ C: r.st.relations.Name, Id: r.doc.Key, - Assert: D{{"life", Alive}, {"unitcount", D{{"$gt", 0}}}}, - Update: D{{"$set", D{{"life", Dying}}}}, + Assert: bson.D{{"life", Alive}, {"unitcount", bson.D{{"$gt", 0}}}}, + Update: bson.D{{"$set", bson.D{{"life", Dying}}}}, }}, false, nil } @@ -169,17 +170,17 @@ Remove: true, } if departingUnit != nil { - relOp.Assert = D{{"life", Dying}, {"unitcount", 1}} + relOp.Assert = bson.D{{"life", Dying}, {"unitcount", 1}} } else { - relOp.Assert = D{{"life", Alive}, {"unitcount", 0}} + relOp.Assert = bson.D{{"life", Alive}, {"unitcount", 0}} } ops := []txn.Op{relOp} for _, ep := range r.doc.Endpoints { if ep.ServiceName == ignoreService { continue } - var asserts D - hasRelation := D{{"relationcount", D{{"$gt", 0}}}} + var asserts bson.D + hasRelation := bson.D{{"relationcount", bson.D{{"$gt", 0}}}} if departingUnit == nil { // We're constructing a destroy operation, either of the relation // or one of its services, and can therefore be assured that both @@ -188,13 +189,13 @@ } else if ep.ServiceName == departingUnit.ServiceName() { // This service must have at least one unit -- the one that's // departing the relation -- so it cannot be ready for removal. - cannotDieYet := D{{"unitcount", D{{"$gt", 0}}}} + cannotDieYet := bson.D{{"unitcount", bson.D{{"$gt", 0}}}} asserts = append(hasRelation, cannotDieYet...) } else { // This service may require immediate removal. svc := &Service{st: r.st} - hasLastRef := D{{"life", Dying}, {"unitcount", 0}, {"relationcount", 1}} - removable := append(D{{"_id", ep.ServiceName}}, hasLastRef...) + hasLastRef := bson.D{{"life", Dying}, {"unitcount", 0}, {"relationcount", 1}} + removable := append(bson.D{{"_id", ep.ServiceName}}, hasLastRef...) if err := r.st.services.Find(removable).One(&svc.doc); err == nil { ops = append(ops, svc.removeOps(hasLastRef)...) continue @@ -203,17 +204,17 @@ } // If not, we must check that this is still the case when the // transaction is applied. - asserts = D{{"$or", []D{ + asserts = bson.D{{"$or", []bson.D{ {{"life", Alive}}, - {{"unitcount", D{{"$gt", 0}}}}, - {{"relationcount", D{{"$gt", 1}}}}, + {{"unitcount", bson.D{{"$gt", 0}}}}, + {{"relationcount", bson.D{{"$gt", 1}}}}, }}} } ops = append(ops, txn.Op{ C: r.st.services.Name, Id: ep.ServiceName, Assert: asserts, - Update: D{{"$inc", D{{"relationcount", -1}}}}, + Update: bson.D{{"$inc", bson.D{{"relationcount", -1}}}}, }) } cleanupOp := r.st.newCleanupOp("settings", fmt.Sprintf("r#%d#", r.Id())) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/relation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/relation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/relation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/relation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,12 +4,12 @@ package state_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" ) type RelationSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/relationunit.go juju-core-1.17.6/src/launchpad.net/juju-core/state/relationunit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/relationunit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/relationunit.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "strings" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/charm" @@ -95,7 +96,7 @@ C: ru.st.relations.Name, Id: relationKey, Assert: isAliveDoc, - Update: D{{"$inc", D{{"unitcount", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"unitcount", 1}}}}, }} // * Create the unit settings in this relation, if they do not already @@ -202,7 +203,7 @@ return nil, "", fmt.Errorf("expected single related endpoint, got %v", related) } serviceName, unitName := related[0].ServiceName, ru.unit.doc.Name - selSubordinate := D{{"service", serviceName}, {"principal", unitName}} + selSubordinate := bson.D{{"service", serviceName}, {"principal", unitName}} var lDoc lifeDoc if err := ru.st.units.Find(selSubordinate).One(&lDoc); err == mgo.ErrNotFound { service, err := ru.st.Service(serviceName) @@ -273,15 +274,15 @@ ops = append(ops, txn.Op{ C: ru.st.relations.Name, Id: ru.relation.doc.Key, - Assert: D{{"life", Alive}}, - Update: D{{"$inc", D{{"unitcount", -1}}}}, + Assert: bson.D{{"life", Alive}}, + Update: bson.D{{"$inc", bson.D{{"unitcount", -1}}}}, }) } else if ru.relation.doc.UnitCount > 1 { ops = append(ops, txn.Op{ C: ru.st.relations.Name, Id: ru.relation.doc.Key, - Assert: D{{"unitcount", D{{"$gt", 1}}}}, - Update: D{{"$inc", D{{"unitcount", -1}}}}, + Assert: bson.D{{"unitcount", bson.D{{"$gt", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"unitcount", -1}}}}, }) } else { relOps, err := ru.relation.removeOps("", ru.unit) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/relationunit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/relationunit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/relationunit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/relationunit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "strconv" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -16,7 +17,6 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type RUs []*state.RelationUnit diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/sequence.go juju-core-1.17.6/src/launchpad.net/juju-core/state/sequence.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/sequence.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/sequence.go 2014-03-20 12:52:38.000000000 +0000 @@ -16,7 +16,7 @@ } func (s *State) sequence(name string) (int, error) { - query := s.db.C("sequence").Find(D{{"_id", name}}) + query := s.db.C("sequence").Find(bson.D{{"_id", name}}) inc := mgo.Change{ Update: bson.M{"$inc": bson.M{"counter": 1}}, Upsert: true, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/service.go juju-core-1.17.6/src/launchpad.net/juju-core/state/service.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/service.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/service.go 2014-03-20 12:52:38.000000000 +0000 @@ -157,7 +157,7 @@ relOps = []txn.Op{{ C: s.st.relations.Name, Id: rel.doc.Key, - Assert: D{{"life", Dying}}, + Assert: bson.D{{"life", Dying}}, }} } else if err != nil { return nil, err @@ -170,7 +170,7 @@ // If the service has no units, and all its known relations will be // removed, the service can also be removed. if s.doc.UnitCount == 0 && s.doc.RelationCount == removeCount { - hasLastRefs := D{{"life", Alive}, {"unitcount", 0}, {"relationcount", removeCount}} + hasLastRefs := bson.D{{"life", Alive}, {"unitcount", 0}, {"relationcount", removeCount}} return append(ops, s.removeOps(hasLastRefs)...), nil } // In all other cases, service removal will be handled as a consequence @@ -180,7 +180,7 @@ // a destroy op for that relation too. In combination, it's enough to // check for count equality: an add/remove will not touch the count, but // will be caught by virtue of being a remove. - notLastRefs := D{ + notLastRefs := bson.D{ {"life", Alive}, {"relationcount", s.doc.RelationCount}, } @@ -190,13 +190,13 @@ // being removed: the difference between 1 unit and 1000 is irrelevant. if s.doc.UnitCount > 0 { ops = append(ops, s.st.newCleanupOp("units", s.doc.Name+"/")) - notLastRefs = append(notLastRefs, D{{"unitcount", D{{"$gt", 0}}}}...) + notLastRefs = append(notLastRefs, bson.D{{"unitcount", bson.D{{"$gt", 0}}}}...) } else { - notLastRefs = append(notLastRefs, D{{"unitcount", 0}}...) + notLastRefs = append(notLastRefs, bson.D{{"unitcount", 0}}...) } - update := D{{"$set", D{{"life", Dying}}}} + update := bson.D{{"$set", bson.D{{"life", Dying}}}} if removeCount != 0 { - decref := D{{"$inc", D{{"relationcount", -removeCount}}}} + decref := bson.D{{"$inc", bson.D{{"relationcount", -removeCount}}}} update = append(update, decref...) } return append(ops, txn.Op{ @@ -209,7 +209,7 @@ // removeOps returns the operations required to remove the service. Supplied // asserts will be included in the operation on the service document. -func (s *Service) removeOps(asserts D) []txn.Op { +func (s *Service) removeOps(asserts bson.D) []txn.Op { ops := []txn.Op{{ C: s.st.services.Name, Id: s.doc.Name, @@ -252,7 +252,7 @@ C: s.st.services.Name, Id: s.doc.Name, Assert: isAliveDoc, - Update: D{{"$set", D{{"exposed", exposed}}}}, + Update: bson.D{{"$set", bson.D{{"exposed", exposed}}}}, }} if err := s.st.runTransaction(ops); err != nil { return fmt.Errorf("cannot set exposed flag for service %q to %v: %v", s, exposed, onAbort(err, errNotAlive)) @@ -406,7 +406,7 @@ } // Build the transaction. - differentCharm := D{{"charmurl", D{{"$ne", ch.URL()}}}} + differentCharm := bson.D{{"charmurl", bson.D{{"$ne", ch.URL()}}}} ops := []txn.Op{ // Old settings shouldn't change oldSettings.assertUnchangedOp(), @@ -419,7 +419,7 @@ C: s.st.services.Name, Id: s.doc.Name, Assert: append(isAliveDoc, differentCharm...), - Update: D{{"$set", D{{"charmurl", ch.URL()}, {"forcecharm", force}}}}, + Update: bson.D{{"$set", bson.D{{"charmurl", ch.URL()}, {"forcecharm", force}}}}, }, } // Add any extra peer relations that need creation. @@ -435,7 +435,7 @@ return nil, err } // Make sure the relation count does not change. - sameRelCount := D{{"relationcount", len(relations)}} + sameRelCount := bson.D{{"relationcount", len(relations)}} ops = append(ops, peerOps...) // Update the relation count as well. @@ -443,7 +443,7 @@ C: s.st.services.Name, Id: s.doc.Name, Assert: append(isAliveDoc, sameRelCount...), - Update: D{{"$inc", D{{"relationcount", len(newPeers)}}}}, + Update: bson.D{{"$inc", bson.D{{"relationcount", len(newPeers)}}}}, }) // Check relations to ensure no active relations are removed. relOps, err := s.checkRelationsOps(ch, relations) @@ -469,17 +469,17 @@ for i := 0; i < 5; i++ { var ops []txn.Op // Make sure the service doesn't have this charm already. - sel := D{{"_id", s.doc.Name}, {"charmurl", ch.URL()}} + sel := bson.D{{"_id", s.doc.Name}, {"charmurl", ch.URL()}} if count, err := s.st.services.Find(sel).Count(); err != nil { return err } else if count == 1 { // Charm URL already set; just update the force flag. - sameCharm := D{{"charmurl", ch.URL()}} + sameCharm := bson.D{{"charmurl", ch.URL()}} ops = []txn.Op{{ C: s.st.services.Name, Id: s.doc.Name, Assert: append(isAliveDoc, sameCharm...), - Update: D{{"$set", D{{"forcecharm", force}}}}, + Update: bson.D{{"$set", bson.D{{"forcecharm", force}}}}, }} } else { // Change the charm URL. @@ -529,9 +529,9 @@ // newUnitName returns the next unit name. func (s *Service) newUnitName() (string, error) { - change := mgo.Change{Update: D{{"$inc", D{{"unitseq", 1}}}}} + change := mgo.Change{Update: bson.D{{"$inc", bson.D{{"unitseq", 1}}}}} result := serviceDoc{} - if _, err := s.st.services.Find(D{{"_id", s.doc.Name}}).Apply(change, &result); err == mgo.ErrNotFound { + if _, err := s.st.services.Find(bson.D{{"_id", s.doc.Name}}).Apply(change, &result); err == mgo.ErrNotFound { return "", errors.NotFoundf("service %q", s) } else if err != nil { return "", fmt.Errorf("cannot increment unit sequence: %v", err) @@ -545,7 +545,7 @@ // and only if s is a subordinate service. Only one subordinate of a given // service will be assigned to a given principal. The asserts param can be used // to include additional assertions for the service document. -func (s *Service) addUnitOps(principalName string, asserts D) (string, []txn.Op, error) { +func (s *Service) addUnitOps(principalName string, asserts bson.D) (string, []txn.Op, error) { if s.doc.Subordinate && principalName == "" { return "", nil, fmt.Errorf("service is a subordinate") } else if !s.doc.Subordinate && principalName != "" { @@ -578,16 +578,16 @@ C: s.st.services.Name, Id: s.doc.Name, Assert: append(isAliveDoc, asserts...), - Update: D{{"$inc", D{{"unitcount", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"unitcount", 1}}}}, }} if s.doc.Subordinate { ops = append(ops, txn.Op{ C: s.st.units.Name, Id: principalName, Assert: append(isAliveDoc, bson.DocElem{ - "subordinates", D{{"$not", bson.RegEx{Pattern: "^" + s.doc.Name + "/"}}}, + "subordinates", bson.D{{"$not", bson.RegEx{Pattern: "^" + s.doc.Name + "/"}}}, }), - Update: D{{"$addToSet", D{{"subordinates", name}}}}, + Update: bson.D{{"$addToSet", bson.D{{"subordinates", name}}}}, }) } else { scons, err := s.Constraints() @@ -644,24 +644,24 @@ // removeUnitOps returns the operations necessary to remove the supplied unit, // assuming the supplied asserts apply to the unit document. -func (s *Service) removeUnitOps(u *Unit, asserts D) ([]txn.Op, error) { +func (s *Service) removeUnitOps(u *Unit, asserts bson.D) ([]txn.Op, error) { var ops []txn.Op if s.doc.Subordinate { ops = append(ops, txn.Op{ C: s.st.units.Name, Id: u.doc.Principal, Assert: txn.DocExists, - Update: D{{"$pull", D{{"subordinates", u.doc.Name}}}}, + Update: bson.D{{"$pull", bson.D{{"subordinates", u.doc.Name}}}}, }) } else if u.doc.MachineId != "" { ops = append(ops, txn.Op{ C: s.st.machines.Name, Id: u.doc.MachineId, Assert: txn.DocExists, - Update: D{{"$pull", D{{"principals", u.doc.Name}}}}, + Update: bson.D{{"$pull", bson.D{{"principals", u.doc.Name}}}}, }) } - observedFieldsMatch := D{ + observedFieldsMatch := bson.D{ {"charmurl", u.doc.CharmURL}, {"machineid", u.doc.MachineId}, } @@ -685,22 +685,22 @@ ops = append(ops, decOps...) } if s.doc.Life == Dying && s.doc.RelationCount == 0 && s.doc.UnitCount == 1 { - hasLastRef := D{{"life", Dying}, {"relationcount", 0}, {"unitcount", 1}} + hasLastRef := bson.D{{"life", Dying}, {"relationcount", 0}, {"unitcount", 1}} return append(ops, s.removeOps(hasLastRef)...), nil } svcOp := txn.Op{ C: s.st.services.Name, Id: s.doc.Name, - Update: D{{"$inc", D{{"unitcount", -1}}}}, + Update: bson.D{{"$inc", bson.D{{"unitcount", -1}}}}, } if s.doc.Life == Alive { - svcOp.Assert = D{{"life", Alive}, {"unitcount", D{{"$gt", 0}}}} + svcOp.Assert = bson.D{{"life", Alive}, {"unitcount", bson.D{{"$gt", 0}}}} } else { - svcOp.Assert = D{ + svcOp.Assert = bson.D{ {"life", Dying}, - {"$or", []D{ - {{"unitcount", D{{"$gt", 1}}}}, - {{"relationcount", D{{"$gt", 0}}}}, + {"$or", []bson.D{ + {{"unitcount", bson.D{{"$gt", 1}}}}, + {{"relationcount", bson.D{{"$gt", 0}}}}, }}, } } @@ -713,7 +713,7 @@ return nil, fmt.Errorf("%q is not a valid unit name", name) } udoc := &unitDoc{} - sel := D{{"_id", name}, {"service", s.doc.Name}} + sel := bson.D{{"_id", name}, {"service", s.doc.Name}} if err := s.st.units.Find(sel).One(udoc); err != nil { return nil, fmt.Errorf("cannot get unit %q from service %q: %v", name, s.doc.Name, err) } @@ -723,7 +723,7 @@ // AllUnits returns all units of the service. func (s *Service) AllUnits() (units []*Unit, err error) { docs := []unitDoc{} - err = s.st.units.Find(D{{"service", s.doc.Name}}).All(&docs) + err = s.st.units.Find(bson.D{{"service", s.doc.Name}}).All(&docs) if err != nil { return nil, fmt.Errorf("cannot get all units from service %q: %v", s, err) } @@ -741,7 +741,7 @@ func serviceRelations(st *State, name string) (relations []*Relation, err error) { defer utils.ErrorContextf(&err, "can't get relations for service %q", name) docs := []relationDoc{} - err = st.relations.Find(D{{"endpoints.servicename", name}}).All(&docs) + err = st.relations.Find(bson.D{{"endpoints.servicename", name}}).All(&docs) if err != nil { return nil, err } @@ -844,7 +844,7 @@ C: st.settingsrefs.Name, Id: key, Assert: txn.DocExists, - Update: D{{"$inc", D{{"refcount", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"refcount", 1}}}}, }, nil } @@ -864,7 +864,7 @@ return []txn.Op{{ C: st.settingsrefs.Name, Id: key, - Assert: D{{"refcount", 1}}, + Assert: bson.D{{"refcount", 1}}, Remove: true, }, { C: st.settings.Name, @@ -875,8 +875,8 @@ return []txn.Op{{ C: st.settingsrefs.Name, Id: key, - Assert: D{{"refcount", D{{"$gt", 1}}}}, - Update: D{{"$inc", D{{"refcount", -1}}}}, + Assert: bson.D{{"refcount", bson.D{{"$gt", 1}}}}, + Update: bson.D{{"$inc", bson.D{{"refcount", -1}}}}, }}, nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/service_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/service_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/service_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/service_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "sort" + jc "github.com/juju/testing/checkers" "labix.org/v2/mgo" gc "launchpad.net/gocheck" @@ -15,7 +16,6 @@ "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type ServiceSuite struct { @@ -580,13 +580,13 @@ }, }) - adminEP, err := riak.Endpoint("admin") + adminEP, err := riak.Endpoint(state.AdminUser) c.Assert(err, gc.IsNil) c.Assert(adminEP, gc.DeepEquals, state.Endpoint{ ServiceName: "myriak", Relation: charm.Relation{ Interface: "http", - Name: "admin", + Name: state.AdminUser, Role: charm.RoleProvider, Scope: charm.ScopeGlobal, }, diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/settings.go juju-core-1.17.6/src/launchpad.net/juju-core/state/settings.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/settings.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/settings.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "strings" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/errors" @@ -167,7 +168,7 @@ C: c.st.settings.Name, Id: c.key, Assert: txn.DocExists, - Update: D{ + Update: bson.D{ {"$set", updates}, {"$unset", deletions}, }, @@ -317,7 +318,7 @@ } newValues := copyMap(values, escapeReplacer.Replace) op := s.assertUnchangedOp() - op.Update = D{ + op.Update = bson.D{ {"$set", newValues}, {"$unset", deletes}, } @@ -335,6 +336,6 @@ return txn.Op{ C: s.st.settings.Name, Id: s.key, - Assert: D{{"txn-revno", s.txnRevno}}, + Assert: bson.D{{"txn-revno", s.txnRevno}}, } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/settings_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/settings_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/settings_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/settings_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,12 +4,12 @@ package state import ( + jc "github.com/juju/testing/checkers" "labix.org/v2/mgo/txn" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/state.go juju-core-1.17.6/src/launchpad.net/juju-core/state/state.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/state.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/state.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "strings" "sync" - "github.com/loggo/loggo" + "github.com/juju/loggo" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" @@ -34,11 +34,11 @@ var logger = loggo.GetLogger("juju.state") -// TODO(niemeyer): This must not be exported. -type D []bson.DocElem - // BootstrapNonce is used as a nonce for the state server machine. -const BootstrapNonce = "user-admin:bootstrap" +const ( + BootstrapNonce = "user-admin:bootstrap" + AdminUser = "admin" +) // State represents the state of an environment // managed by juju. @@ -184,11 +184,11 @@ matchCurrent := "^" + regexp.QuoteMeta(currentVersion) + "-" matchNew := "^" + regexp.QuoteMeta(newVersion) + "-" // Get all machines and units with a different or empty version. - sel := D{{"$or", []D{ - {{"tools", D{{"$exists", false}}}}, - {{"$and", []D{ - {{"tools.version", D{{"$not", bson.RegEx{matchCurrent, ""}}}}}, - {{"tools.version", D{{"$not", bson.RegEx{matchNew, ""}}}}}, + sel := bson.D{{"$or", []bson.D{ + {{"tools", bson.D{{"$exists", false}}}}, + {{"$and", []bson.D{ + {{"tools.version", bson.D{{"$not", bson.RegEx{matchCurrent, ""}}}}}, + {{"tools.version", bson.D{{"$not", bson.RegEx{matchNew, ""}}}}}, }}}, }}} var agentTags []string @@ -196,7 +196,7 @@ var doc struct { Id string `bson:"_id"` } - iter := collection.Find(sel).Select(D{{"_id", 1}}).Iter() + iter := collection.Find(sel).Select(bson.D{{"_id", 1}}).Iter() for iter.Next(&doc) { switch collection.Name { case "machines": @@ -244,8 +244,8 @@ ops := []txn.Op{{ C: st.settings.Name, Id: environGlobalKey, - Assert: D{{"txn-revno", settings.txnRevno}}, - Update: D{{"$set", D{{"agent-version", newVersion.String()}}}}, + Assert: bson.D{{"txn-revno", settings.txnRevno}}, + Update: bson.D{{"$set", bson.D{{"agent-version", newVersion.String()}}}}, }} if err := st.runTransaction(ops); err == nil { return nil @@ -256,12 +256,33 @@ return ErrExcessiveContention } -// SetEnvironConfig replaces the current configuration of the -// environment with the provided configuration. -func (st *State) SetEnvironConfig(cfg, old *config.Config) error { - if err := checkEnvironConfig(cfg); err != nil { - return err +func (st *State) buildAndValidateEnvironConfig(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) (validCfg *config.Config, err error) { + newConfig, err := oldConfig.Apply(updateAttrs) + if err != nil { + return nil, err + } + if len(removeAttrs) != 0 { + newConfig, err = newConfig.Remove(removeAttrs) + if err != nil { + return nil, err + } + } + if err := checkEnvironConfig(newConfig); err != nil { + return nil, err + } + return st.validate(newConfig, oldConfig) +} + +type ValidateConfigFunc func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error + +// UpateEnvironConfig adds, updates or removes attributes in the current +// configuration of the environment with the provided updateAttrs and +// removeAttrs. +func (st *State) UpdateEnvironConfig(updateAttrs map[string]interface{}, removeAttrs []string, additionalValidation ValidateConfigFunc) error { + if len(updateAttrs)+len(removeAttrs) == 0 { + return nil } + // TODO(axw) 2013-12-6 #1167616 // Ensure that the settings on disk have not changed // underneath us. The settings changes are actually @@ -272,13 +293,30 @@ if err != nil { return err } - newattrs := cfg.AllAttrs() - for k, _ := range old.AllAttrs() { - if _, ok := newattrs[k]; !ok { + + // Get the existing environment config from state. + oldConfig, err := config.New(config.NoDefaults, settings.Map()) + if err != nil { + return err + } + if additionalValidation != nil { + err = additionalValidation(updateAttrs, removeAttrs, oldConfig) + if err != nil { + return err + } + } + validCfg, err := st.buildAndValidateEnvironConfig(updateAttrs, removeAttrs, oldConfig) + if err != nil { + return err + } + + validAttrs := validCfg.AllAttrs() + for k, _ := range oldConfig.AllAttrs() { + if _, ok := validAttrs[k]; !ok { settings.Delete(k) } } - settings.Update(newattrs) + settings.Update(validAttrs) _, err = settings.Write() return err } @@ -371,7 +409,7 @@ // Machine returns the machine with the given id. func (st *State) Machine(id string) (*Machine, error) { mdoc := &machineDoc{} - sel := D{{"_id", id}} + sel := bson.D{{"_id", id}} err := st.machines.Find(sel).One(mdoc) if err == mgo.ErrNotFound { return nil, errors.NotFoundf("machine %s", id) @@ -462,7 +500,7 @@ // check for that situation and update the existing charm record // if necessary, otherwise add a new record. var existing charmDoc - err = st.charms.Find(D{{"_id", curl.String()}, {"placeholder", true}}).One(&existing) + err = st.charms.Find(bson.D{{"_id", curl.String()}, {"placeholder", true}}).One(&existing) if err == mgo.ErrNotFound { cdoc := &charmDoc{ URL: curl, @@ -486,10 +524,10 @@ // to storage and placeholders are never returned. func (st *State) Charm(curl *charm.URL) (*Charm, error) { cdoc := &charmDoc{} - what := D{ + what := bson.D{ {"_id", curl}, - {"placeholder", D{{"$ne", true}}}, - {"pendingupload", D{{"$ne", true}}}, + {"placeholder", bson.D{{"$ne", true}}}, + {"pendingupload", bson.D{{"$ne", true}}}, } err := st.charms.Find(what).One(&cdoc) if err == mgo.ErrNotFound { @@ -510,7 +548,7 @@ noRevURL := curl.WithRevision(-1) curlRegex := "^" + regexp.QuoteMeta(noRevURL.String()) var docs []charmDoc - err := st.charms.Find(D{{"_id", D{{"$regex", curlRegex}}}, {"placeholder", true}}).All(&docs) + err := st.charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).All(&docs) if err != nil { return nil, fmt.Errorf("cannot get charm %q: %v", curl, err) } @@ -548,7 +586,7 @@ for attempt := 0; attempt < 3; attempt++ { // Find the highest revision of that charm in state. var docs []charmDoc - err = st.charms.Find(D{{"_id", D{{"$regex", curlRegex}}}}).Select(D{{"_id", 1}}).All(&docs) + err = st.charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}}).Select(bson.D{{"_id", 1}}).All(&docs) if err != nil { return nil, err } @@ -641,12 +679,12 @@ ops = []txn.Op{{ C: st.charms.Name, Id: curl, - Assert: D{ + Assert: bson.D{ {"bundlesha256", ""}, {"pendingupload", false}, {"placeholder", true}, }, - Update: D{{"$set", D{ + Update: bson.D{{"$set", bson.D{ {"pendingupload", true}, {"placeholder", false}, }}}, @@ -678,8 +716,8 @@ } var ( - stillPending = D{{"pendingupload", true}} - stillPlaceholder = D{{"placeholder", true}} + stillPending = bson.D{{"pendingupload", true}} + stillPlaceholder = bson.D{{"placeholder", true}} ) // AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which @@ -697,7 +735,7 @@ for attempt := 0; attempt < 3; attempt++ { // See if the charm already exists in state and exit early if that's the case. var doc charmDoc - err = st.charms.Find(D{{"_id", curl.String()}}).Select(D{{"_id", 1}}).One(&doc) + err = st.charms.Find(bson.D{{"_id", curl.String()}}).Select(bson.D{{"_id", 1}}).One(&doc) if err != nil && err != mgo.ErrNotFound { return err } @@ -745,7 +783,7 @@ curlRegex := "^" + regexp.QuoteMeta(noRevURL.String()) var docs []charmDoc err := st.charms.Find( - D{{"_id", D{{"$regex", curlRegex}}}, {"placeholder", true}}).Select(D{{"_id", 1}}).All(&docs) + bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).Select(bson.D{{"_id", 1}}).All(&docs) if err != nil { return nil, err } @@ -815,7 +853,7 @@ func (st *State) updateCharmDoc( ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string, preReq interface{}) (*Charm, error) { - updateFields := D{{"$set", D{ + updateFields := bson.D{{"$set", bson.D{ {"meta", ch.Meta()}, {"config", ch.Config()}, {"bundleurl", bundleURL}, @@ -968,7 +1006,7 @@ return nil, fmt.Errorf("%q is not a valid service name", name) } sdoc := &serviceDoc{} - sel := D{{"_id", name}} + sel := bson.D{{"_id", name}} err = st.services.Find(sel).One(sdoc) if err == mgo.ErrNotFound { return nil, errors.NotFoundf("service %q", name) @@ -982,7 +1020,7 @@ // AllServices returns all deployed services in the environment. func (st *State) AllServices() (services []*Service, err error) { sdocs := []serviceDoc{} - err = st.services.Find(D{}).All(&sdocs) + err = st.services.Find(bson.D{}).All(&sdocs) if err != nil { return nil, fmt.Errorf("cannot get all services") } @@ -1164,8 +1202,8 @@ ops = append(ops, txn.Op{ C: st.services.Name, Id: ep.ServiceName, - Assert: D{{"life", Alive}, {"charmurl", ch.URL()}}, - Update: D{{"$inc", D{{"relationcount", 1}}}}, + Assert: bson.D{{"life", Alive}, {"charmurl", ch.URL()}}, + Update: bson.D{{"$inc", bson.D{{"relationcount", 1}}}}, }) } if matchSeries && len(series) != 1 { @@ -1211,7 +1249,7 @@ // be derived unambiguously from the relation's endpoints). func (st *State) KeyRelation(key string) (*Relation, error) { doc := relationDoc{} - err := st.relations.Find(D{{"_id", key}}).One(&doc) + err := st.relations.Find(bson.D{{"_id", key}}).One(&doc) if err == mgo.ErrNotFound { return nil, errors.NotFoundf("relation %q", key) } @@ -1224,7 +1262,7 @@ // Relation returns the existing relation with the given id. func (st *State) Relation(id int) (*Relation, error) { doc := relationDoc{} - err := st.relations.Find(D{{"id", id}}).One(&doc) + err := st.relations.Find(bson.D{{"id", id}}).One(&doc) if err == mgo.ErrNotFound { return nil, errors.NotFoundf("relation %d", id) } @@ -1294,19 +1332,19 @@ // all subsequent attempts to access the state must // be authorized; otherwise no authorization is required. func (st *State) SetAdminMongoPassword(password string) error { - admin := st.db.Session.DB("admin") + admin := st.db.Session.DB(AdminUser) if password != "" { // On 2.2+, we get a "need to login" error without a code when // adding the first user because we go from no-auth+no-login to // auth+no-login. Not great. Hopefully being fixed in 2.4. - if err := admin.AddUser("admin", password, false); err != nil && err.Error() != "need to login" { + if err := admin.AddUser(AdminUser, password, false); err != nil && err.Error() != "need to login" { return fmt.Errorf("cannot set admin password: %v", err) } - if err := admin.Login("admin", password); err != nil { + if err := admin.Login(AdminUser, password); err != nil { return fmt.Errorf("cannot login after setting password: %v", err) } } else { - if err := admin.RemoveUser("admin"); err != nil && err != mgo.ErrNotFound { + if err := admin.RemoveUser(AdminUser); err != nil && err != mgo.ErrNotFound { return fmt.Errorf("cannot disable admin password: %v", err) } } @@ -1347,7 +1385,7 @@ // the currently configured state server machines. func (st *State) StateServerInfo() (*StateServerInfo, error) { var doc stateServersDoc - err := st.stateServers.Find(D{{"_id", environGlobalKey}}).One(&doc) + err := st.stateServers.Find(bson.D{{"_id", environGlobalKey}}).One(&doc) if err != nil { return nil, fmt.Errorf("cannot get state servers document: %v", err) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/state_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/state_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/state_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/state_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,7 @@ "strconv" "time" + jc "github.com/juju/testing/checkers" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" gc "launchpad.net/gocheck" @@ -25,13 +26,10 @@ "launchpad.net/juju-core/state/api/params" statetesting "launchpad.net/juju-core/state/testing" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) -type D []bson.DocElem - var goodPassword = "foo-12345678901234567890" var alternatePassword = "bar-12345678901234567890" @@ -111,7 +109,7 @@ fmt.Sprintf("10.0.0.3:%d", envConfig.StatePort()), }) - addrs, err = s.State.APIAddresses() + addrs, err = s.State.APIAddressesFromMachines() c.Assert(err, gc.IsNil) c.Assert(addrs, gc.HasLen, 3) c.Assert(addrs, jc.SameContents, []string{ @@ -299,7 +297,7 @@ c.Assert(err, gc.IsNil) }, After: func() { - err := s.charms.UpdateId(curl, D{{"$set", D{ + err := s.charms.UpdateId(curl, bson.D{{"$set", bson.D{ {"bundlesha256", "fake"}}, }}) c.Assert(err, gc.IsNil) @@ -1032,7 +1030,7 @@ func (s *StateSuite) TestAddServiceNoTag(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - _, err := s.State.AddService("wordpress", "admin", charm) + _, err := s.State.AddService("wordpress", state.AdminUser, charm) c.Assert(err, gc.ErrorMatches, "cannot add service \"wordpress\": Invalid ownertag admin") } @@ -1250,18 +1248,20 @@ } func (s *StateSuite) TestEnvironConfig(c *gc.C) { - cfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - change, err := cfg.Apply(map[string]interface{}{ + attrs := map[string]interface{}{ "authorized-keys": "different-keys", "arbitrary-key": "shazam!", - }) + } + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + err = s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(change, cfg) + cfg, err = cfg.Apply(attrs) c.Assert(err, gc.IsNil) - cfg, err = s.State.EnvironConfig() + oldCfg, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) - c.Assert(cfg.AllAttrs(), gc.DeepEquals, change.AllAttrs()) + + c.Assert(oldCfg, gc.DeepEquals, cfg) } func (s *StateSuite) TestEnvironConstraints(c *gc.C) { @@ -1455,8 +1455,8 @@ machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, gc.IsNil) err = s.machines.Update( - D{{"_id", machine.Id()}}, - D{{"$unset", D{{"containertype", 1}}}}, + bson.D{{"_id", machine.Id()}}, + bson.D{{"$unset", bson.D{{"containertype", 1}}}}, ) c.Assert(err, gc.IsNil) @@ -1681,6 +1681,37 @@ }) } +func (s *StateSuite) TestAdditionalValidation(c *gc.C) { + updateAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} + configValidator1 := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { + c.Assert(updateAttrs, gc.DeepEquals, map[string]interface{}{"logging-config": "juju=ERROR"}) + if _, found := updateAttrs["logging-config"]; found { + return fmt.Errorf("cannot change logging-config") + } + return nil + } + removeAttrs := []string{"logging-config"} + configValidator2 := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { + c.Assert(removeAttrs, gc.DeepEquals, []string{"logging-config"}) + for _, i := range removeAttrs { + if i == "logging-config" { + return fmt.Errorf("cannot remove logging-config") + } + } + return nil + } + configValidator3 := func(updateAttrs map[string]interface{}, removeAttrs []string, oldConfig *config.Config) error { + return nil + } + + err := s.State.UpdateEnvironConfig(updateAttrs, nil, configValidator1) + c.Assert(err, gc.ErrorMatches, "cannot change logging-config") + err = s.State.UpdateEnvironConfig(nil, removeAttrs, configValidator2) + c.Assert(err, gc.ErrorMatches, "cannot remove logging-config") + err = s.State.UpdateEnvironConfig(updateAttrs, nil, configValidator3) + c.Assert(err, gc.IsNil) +} + type attrs map[string]interface{} func (s *StateSuite) TestWatchEnvironConfig(c *gc.C) { @@ -1699,11 +1730,10 @@ assertChange := func(change attrs) { cfg, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) + cfg, err = cfg.Apply(change) + c.Assert(err, gc.IsNil) if change != nil { - oldcfg := cfg - cfg, err = cfg.Apply(change) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) + err = s.State.UpdateEnvironConfig(change, nil, nil) c.Assert(err, gc.IsNil) } s.State.StartSync() @@ -1761,7 +1791,6 @@ func (s *StateSuite) TestWatchEnvironConfigCorruptConfig(c *gc.C) { cfg, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) - oldcfg := cfg // Corrupt the environment configuration. settings := s.Session.DB("juju").C("settings") @@ -1797,9 +1826,11 @@ } // Fix the configuration. - err = s.State.SetEnvironConfig(cfg, oldcfg) + err = settings.UpdateId("e", bson.D{{"$set", bson.D{{"name", "foo"}}}}) c.Assert(err, gc.IsNil) fixed := cfg.AllAttrs() + err = s.State.UpdateEnvironConfig(fixed, nil, nil) + c.Assert(err, gc.IsNil) s.State.StartSync() select { @@ -2559,9 +2590,7 @@ func (s *StateSuite) changeEnviron(c *gc.C, envConfig *config.Config, name string, value interface{}) { attrs := envConfig.AllAttrs() attrs[name] = value - newConfig, err := config.New(config.NoDefaults, attrs) - c.Assert(err, gc.IsNil) - c.Assert(s.State.SetEnvironConfig(newConfig, envConfig), gc.IsNil) + c.Assert(s.State.UpdateEnvironConfig(attrs, nil, nil), gc.IsNil) } func (s *StateSuite) assertAgentVersion(c *gc.C, envConfig *config.Config, vers string) { @@ -2690,6 +2719,27 @@ c.Assert(info, gc.DeepEquals, expectStateServerInfo) } +func (s *StateSuite) TestOpenCreatesAPIHostPortsDoc(c *gc.C) { + // Delete the stateServers collection to pretend this + // is an older environment that had not created it + // already. + err := s.stateServers.DropCollection() + c.Assert(err, gc.IsNil) + + // Sanity check that we have in fact deleted the right info. + addrs, err := s.State.APIHostPorts() + c.Assert(err, gc.NotNil) + c.Assert(addrs, gc.IsNil) + + st, err := state.Open(state.TestingStateInfo(), state.TestingDialOpts(), state.Policy(nil)) + c.Assert(err, gc.IsNil) + defer st.Close() + + addrs, err = s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(addrs, gc.HasLen, 0) +} + func (s *StateSuite) TestReopenWithNoMachines(c *gc.C) { info, err := s.State.StateServerInfo() c.Assert(err, gc.IsNil) @@ -2799,3 +2849,78 @@ func newUint64(i uint64) *uint64 { return &i } + +func (s *StateSuite) TestSetAPIHostPorts(c *gc.C) { + addrs, err := s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(addrs, gc.HasLen, 0) + + newHostPorts := [][]instance.HostPort{{{ + Address: instance.Address{ + Value: "0.2.4.6", + Type: instance.Ipv4Address, + NetworkName: "net", + NetworkScope: instance.NetworkCloudLocal, + }, + Port: 1, + }, { + Address: instance.Address{ + Value: "0.4.8.16", + Type: instance.Ipv4Address, + NetworkName: "foo", + NetworkScope: instance.NetworkPublic, + }, + Port: 2, + }}, {{ + Address: instance.Address{ + Value: "0.6.1.2", + Type: instance.Ipv4Address, + NetworkName: "net", + NetworkScope: instance.NetworkCloudLocal, + }, + Port: 5, + }}} + err = s.State.SetAPIHostPorts(newHostPorts) + c.Assert(err, gc.IsNil) + + gotHostPorts, err := s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(gotHostPorts, jc.DeepEquals, newHostPorts) + + newHostPorts = [][]instance.HostPort{{{ + Address: instance.Address{ + Value: "0.2.4.6", + Type: instance.Ipv6Address, + NetworkName: "net", + NetworkScope: instance.NetworkCloudLocal, + }, + Port: 13, + }}} + err = s.State.SetAPIHostPorts(newHostPorts) + c.Assert(err, gc.IsNil) + + gotHostPorts, err = s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(gotHostPorts, jc.DeepEquals, newHostPorts) +} + +func (s *StateSuite) TestWatchAPIHostPorts(c *gc.C) { + w := s.State.WatchAPIHostPorts() + defer statetesting.AssertStop(c, w) + + // Initial event. + wc := statetesting.NewNotifyWatcherC(c, s.State, w) + wc.AssertOneChange() + + err := s.State.SetAPIHostPorts([][]instance.HostPort{{{ + Address: instance.NewAddress("0.1.2.3"), + Port: 99, + }}}) + c.Assert(err, gc.IsNil) + + wc.AssertOneChange() + + // Stop, check closed. + statetesting.AssertStop(c, w) + wc.AssertClosed() +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/status.go juju-core-1.17.6/src/launchpad.net/juju-core/state/status.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/status.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/status.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/errors" @@ -76,7 +77,7 @@ C: st.statuses.Name, Id: globalKey, Assert: txn.DocExists, - Update: D{{"$set", doc}}, + Update: bson.D{{"$set", doc}}, } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/agent.go juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/agent.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/agent.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/agent.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,5 +11,5 @@ // SetAgentVersion sets the current agent version in the state's // environment configuration. func SetAgentVersion(st *state.State, vers version.Number) error { - return UpdateConfig(st, map[string]interface{}{"agent-version": vers.String()}) + return st.UpdateEnvironConfig(map[string]interface{}{"agent-version": vers.String()}, nil, nil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/config.go juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/config.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/config.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/config.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - "launchpad.net/juju-core/state" -) - -// UpdateConfig sets the current agent version in the state's -// environment configuration. -func UpdateConfig(st *state.State, newValues map[string]interface{}) error { - cfg, err := st.EnvironConfig() - if err != nil { - return err - } - newcfg, err := cfg.Apply(newValues) - if err != nil { - return err - } - return st.SetEnvironConfig(newcfg, cfg) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/watcher.go juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/watcher.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/testing/watcher.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/testing/watcher.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,12 +7,12 @@ "sort" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type Stopper interface { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/tools_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/tools_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/tools_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/tools_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,11 +6,11 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/unit.go juju-core-1.17.6/src/launchpad.net/juju-core/state/unit.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/unit.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/unit.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "fmt" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "labix.org/v2/mgo" "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" @@ -185,7 +185,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: notDeadDoc, - Update: D{{"$set", D{{"tools", tools}}}}, + Update: bson.D{{"$set", bson.D{{"tools", tools}}}}, }} if err := u.st.runTransaction(ops); err != nil { return onAbort(err, errDead) @@ -217,7 +217,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: notDeadDoc, - Update: D{{"$set", D{{"passwordhash", passwordHash}}}}, + Update: bson.D{{"$set", bson.D{{"passwordhash", passwordHash}}}}, }} err := u.st.runTransaction(ops) if err != nil { @@ -323,7 +323,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: isAliveDoc, - Update: D{{"$set", D{{"life", Dying}}}}, + Update: bson.D{{"$set", bson.D{{"life", Dying}}}}, }, minUnitsOp} if u.doc.Principal != "" { return setDyingOps, nil @@ -344,7 +344,7 @@ ops := []txn.Op{{ C: u.st.statuses.Name, Id: sdocId, - Assert: D{{"status", params.StatusPending}}, + Assert: bson.D{{"status", params.StatusPending}}, }, minUnitsOp} removeAsserts := append(isAliveDoc, unitHasNoSubordinates...) removeOps, err := u.removeOps(removeAsserts) @@ -360,7 +360,7 @@ // removeOps returns the operations necessary to remove the unit, assuming // the supplied asserts apply to the unit document. -func (u *Unit) removeOps(asserts D) ([]txn.Op, error) { +func (u *Unit) removeOps(asserts bson.D) ([]txn.Op, error) { svc, err := u.st.Service(u.doc.Service) if errors.IsNotFoundError(err) { // If the service has been removed, the unit must already have been. @@ -373,10 +373,10 @@ var ErrUnitHasSubordinates = stderrors.New("unit has subordinates") -var unitHasNoSubordinates = D{{ - "$or", []D{ - {{"subordinates", D{{"$size", 0}}}}, - {{"subordinates", D{{"$exists", false}}}}, +var unitHasNoSubordinates = bson.D{{ + "$or", []bson.D{ + {{"subordinates", bson.D{{"$size", 0}}}}, + {{"subordinates", bson.D{{"$exists", false}}}}, }, }} @@ -396,7 +396,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: append(notDeadDoc, unitHasNoSubordinates...), - Update: D{{"$set", D{{"life", Dead}}}}, + Update: bson.D{{"$set", bson.D{{"life", Dead}}}}, }} if err := u.st.runTransaction(ops); err != txn.ErrAborted { return err @@ -587,7 +587,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: notDeadDoc, - Update: D{{"$addToSet", D{{"ports", port}}}}, + Update: bson.D{{"$addToSet", bson.D{{"ports", port}}}}, }} err = u.st.runTransaction(ops) if err != nil { @@ -613,7 +613,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: notDeadDoc, - Update: D{{"$pull", D{{"ports", port}}}}, + Update: bson.D{{"$pull", bson.D{{"ports", port}}}}, }} err = u.st.runTransaction(ops) if err != nil { @@ -661,7 +661,7 @@ } else if !notDead { return fmt.Errorf("unit %q is dead", u) } - sel := D{{"_id", u.doc.Name}, {"charmurl", curl}} + sel := bson.D{{"_id", u.doc.Name}, {"charmurl", curl}} if count, err := u.st.units.Find(sel).Count(); err != nil { return err } else if count == 1 { @@ -681,14 +681,14 @@ } // Set the new charm URL. - differentCharm := D{{"charmurl", D{{"$ne", curl}}}} + differentCharm := bson.D{{"charmurl", bson.D{{"$ne", curl}}}} ops := []txn.Op{ incOp, { C: u.st.units.Name, Id: u.doc.Name, Assert: append(notDeadDoc, differentCharm...), - Update: D{{"$set", D{{"charmurl", curl}}}}, + Update: bson.D{{"$set", bson.D{{"charmurl", curl}}}}, }} if u.doc.CharmURL != nil { // Drop the reference to the old charm. @@ -771,7 +771,7 @@ return u.doc.MachineId, nil } pudoc := unitDoc{} - err = u.st.units.Find(D{{"_id", u.doc.Principal}}).One(&pudoc) + err = u.st.units.Find(bson.D{{"_id", u.doc.Principal}}).One(&pudoc) if err == mgo.ErrNotFound { return "", errors.NotFoundf("principal unit %q of %q", u.doc.Principal, u) } else if err != nil { @@ -821,26 +821,26 @@ if !canHost { return fmt.Errorf("machine %q cannot host units", m) } - assert := append(isAliveDoc, D{ - {"$or", []D{ + assert := append(isAliveDoc, bson.D{ + {"$or", []bson.D{ {{"machineid", ""}}, {{"machineid", m.Id()}}, }}, }...) massert := isAliveDoc if unused { - massert = append(massert, D{{"clean", D{{"$ne", false}}}}...) + massert = append(massert, bson.D{{"clean", bson.D{{"$ne", false}}}}...) } ops := []txn.Op{{ C: u.st.units.Name, Id: u.doc.Name, Assert: assert, - Update: D{{"$set", D{{"machineid", m.doc.Id}}}}, + Update: bson.D{{"$set", bson.D{{"machineid", m.doc.Id}}}}, }, { C: u.st.machines.Name, Id: m.doc.Id, Assert: massert, - Update: D{{"$addToSet", D{{"principals", u.doc.Name}}}, {"$set", D{{"clean", false}}}}, + Update: bson.D{{"$addToSet", bson.D{{"principals", u.doc.Name}}}, {"$set", bson.D{{"clean", false}}}}, }} err = u.st.runTransaction(ops) if err == nil { @@ -917,20 +917,20 @@ ops = append(ops, txn.Op{ C: u.st.machines.Name, Id: parentId, - Assert: D{{"clean", true}}, + Assert: bson.D{{"clean", true}}, }, txn.Op{ C: u.st.containerRefs.Name, Id: parentId, - Assert: D{hasNoContainersTerm}, + Assert: bson.D{hasNoContainersTerm}, }) } - isUnassigned := D{{"machineid", ""}} + isUnassigned := bson.D{{"machineid", ""}} asserts := append(isAliveDoc, isUnassigned...) ops = append(ops, txn.Op{ C: u.st.units.Name, Id: u.doc.Name, Assert: asserts, - Update: D{{"$set", D{{"machineid", mdoc.Id}}}}, + Update: bson.D{{"$set", bson.D{{"machineid", mdoc.Id}}}}, }) err = u.st.runTransaction(ops) @@ -1090,15 +1090,15 @@ } var hasContainerTerm = bson.DocElem{ - "$and", []D{ - {{"children", D{{"$not", D{{"$size", 0}}}}}}, - {{"children", D{{"$exists", true}}}}, + "$and", []bson.D{ + {{"children", bson.D{{"$not", bson.D{{"$size", 0}}}}}}, + {{"children", bson.D{{"$exists", true}}}}, }} var hasNoContainersTerm = bson.DocElem{ - "$or", []D{ - {{"children", D{{"$size", 0}}}}, - {{"children", D{{"$exists", false}}}}, + "$or", []bson.D{ + {{"children", bson.D{{"$size", 0}}}}, + {{"children", bson.D{{"$exists", false}}}}, }} // findCleanMachineQuery returns a Mongo query to find clean (and possibly empty) machines with @@ -1109,7 +1109,7 @@ // If we need empty machines, first build up a list of machine ids which have containers // so we can exclude those. if requireEmpty { - err := u.st.containerRefs.Find(D{hasContainerTerm}).All(&containerRefs) + err := u.st.containerRefs.Find(bson.D{hasContainerTerm}).All(&containerRefs) if err != nil { return nil, err } @@ -1118,12 +1118,12 @@ for i, cref := range containerRefs { machinesWithContainers[i] = cref.Id } - terms := D{ + terms := bson.D{ {"life", Alive}, {"series", u.doc.Series}, {"jobs", []MachineJob{JobHostUnits}}, {"clean", true}, - {"_id", D{{"$nin", machinesWithContainers}}}, + {"_id", bson.D{{"$nin", machinesWithContainers}}}, } // Add the container filter term if necessary. var containerType instance.ContainerType @@ -1144,24 +1144,24 @@ // be suitable, but we don't know that right now and it's best // to err on the side of caution and exclude such machines. var suitableInstanceData []instanceData - var suitableTerms D + var suitableTerms bson.D if cons.Arch != nil && *cons.Arch != "" { suitableTerms = append(suitableTerms, bson.DocElem{"arch", *cons.Arch}) } if cons.Mem != nil && *cons.Mem > 0 { - suitableTerms = append(suitableTerms, bson.DocElem{"mem", D{{"$gte", *cons.Mem}}}) + suitableTerms = append(suitableTerms, bson.DocElem{"mem", bson.D{{"$gte", *cons.Mem}}}) } if cons.RootDisk != nil && *cons.RootDisk > 0 { - suitableTerms = append(suitableTerms, bson.DocElem{"rootdisk", D{{"$gte", *cons.RootDisk}}}) + suitableTerms = append(suitableTerms, bson.DocElem{"rootdisk", bson.D{{"$gte", *cons.RootDisk}}}) } if cons.CpuCores != nil && *cons.CpuCores > 0 { - suitableTerms = append(suitableTerms, bson.DocElem{"cpucores", D{{"$gte", *cons.CpuCores}}}) + suitableTerms = append(suitableTerms, bson.DocElem{"cpucores", bson.D{{"$gte", *cons.CpuCores}}}) } if cons.CpuPower != nil && *cons.CpuPower > 0 { - suitableTerms = append(suitableTerms, bson.DocElem{"cpupower", D{{"$gte", *cons.CpuPower}}}) + suitableTerms = append(suitableTerms, bson.DocElem{"cpupower", bson.D{{"$gte", *cons.CpuPower}}}) } if cons.Tags != nil && len(*cons.Tags) > 0 { - suitableTerms = append(suitableTerms, bson.DocElem{"tags", D{{"$all", *cons.Tags}}}) + suitableTerms = append(suitableTerms, bson.DocElem{"tags", bson.D{{"$all", *cons.Tags}}}) } if len(suitableTerms) > 0 { err := u.st.instanceData.Find(suitableTerms).Select(bson.M{"_id": 1}).All(&suitableInstanceData) @@ -1172,7 +1172,7 @@ for i, m := range suitableInstanceData { suitableIds[i] = m.Id } - terms = append(terms, bson.DocElem{"_id", D{{"$in", suitableIds}}}) + terms = append(terms, bson.DocElem{"_id", bson.D{{"$in", suitableIds}}}) } return u.st.machines.Find(terms), nil } @@ -1237,14 +1237,14 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: txn.DocExists, - Update: D{{"$set", D{{"machineid", ""}}}}, + Update: bson.D{{"$set", bson.D{{"machineid", ""}}}}, }} if u.doc.MachineId != "" { ops = append(ops, txn.Op{ C: u.st.machines.Name, Id: u.doc.MachineId, Assert: txn.DocExists, - Update: D{{"$pull", D{{"principals", u.doc.Name}}}}, + Update: bson.D{{"$pull", bson.D{{"principals", u.doc.Name}}}}, }) } err = u.st.runTransaction(ops) @@ -1261,7 +1261,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: txn.DocExists, - Update: D{{"$set", D{{"publicaddress", address}}}}, + Update: bson.D{{"$set", bson.D{{"publicaddress", address}}}}, }} if err := u.st.runTransaction(ops); err != nil { return fmt.Errorf("cannot set public address of unit %q: %v", u, onAbort(err, errors.NotFoundf("unit"))) @@ -1276,7 +1276,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: notDeadDoc, - Update: D{{"$set", D{{"privateaddress", address}}}}, + Update: bson.D{{"$set", bson.D{{"privateaddress", address}}}}, }} err := u.st.runTransaction(ops) if err != nil { @@ -1319,12 +1319,12 @@ return fmt.Errorf("invalid error resolution mode: %q", mode) } // TODO(fwereade): assert unit has error status. - resolvedNotSet := D{{"resolved", ResolvedNone}} + resolvedNotSet := bson.D{{"resolved", ResolvedNone}} ops := []txn.Op{{ C: u.st.units.Name, Id: u.doc.Name, Assert: append(notDeadDoc, resolvedNotSet...), - Update: D{{"$set", D{{"resolved", mode}}}}, + Update: bson.D{{"$set", bson.D{{"resolved", mode}}}}, }} if err := u.st.runTransaction(ops); err == nil { u.doc.Resolved = mode @@ -1347,7 +1347,7 @@ C: u.st.units.Name, Id: u.doc.Name, Assert: txn.DocExists, - Update: D{{"$set", D{{"resolved", ResolvedNone}}}}, + Update: bson.D{{"$set", bson.D{{"resolved", ResolvedNone}}}}, }} err := u.st.runTransaction(ops) if err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/unit_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/unit_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/unit_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/unit_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "strconv" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -15,7 +16,6 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" ) type UnitSuite struct { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/user.go juju-core-1.17.6/src/launchpad.net/juju-core/state/user.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/user.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/user.go 2014-03-20 12:52:38.000000000 +0000 @@ -1,6 +1,3 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - package state import ( @@ -8,6 +5,7 @@ "regexp" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "labix.org/v2/mgo/txn" "launchpad.net/juju-core/errors" @@ -62,7 +60,7 @@ // getUser fetches information about the user with the // given name into the provided userDoc. func (st *State) getUser(name string, udoc *userDoc) error { - err := st.users.Find(D{{"_id", name}}).One(udoc) + err := st.users.Find(bson.D{{"_id", name}}).One(udoc) if err == mgo.ErrNotFound { err = errors.NotFoundf("user %q", name) } @@ -86,6 +84,7 @@ type userDoc struct { Name string `bson:"_id_"` + Deactivated bool // Removing users means they still exist, but are marked deactivated PasswordHash string PasswordSalt string } @@ -118,7 +117,7 @@ ops := []txn.Op{{ C: u.st.users.Name, Id: u.Name(), - Update: D{{"$set", D{{"passwordhash", pwHash}, {"passwordsalt", pwSalt}}}}, + Update: bson.D{{"$set", bson.D{{"passwordhash", pwHash}, {"passwordsalt", pwSalt}}}}, }} if err := u.st.runTransaction(ops); err != nil { return fmt.Errorf("cannot set password of user %q: %v", u.Name(), err) @@ -131,6 +130,10 @@ // PasswordValid returns whether the given password // is valid for the user. func (u *User) PasswordValid(password string) bool { + // If the user is deactivated, no point in carrying on + if u.IsDeactivated() { + return false + } // Since these are potentially set by a User, we intentionally use the // slower pbkdf2 style hashing. Also, we don't expect to have thousands // of Users trying to log in at the same time (which we *do* expect of @@ -146,7 +149,10 @@ // fails because we will try again at the next request logger.Debugf("User %s logged in with CompatSalt resetting password for new salt", u.Name()) - u.SetPassword(password) + err := u.SetPassword(password) + if err != nil { + logger.Errorf("Cannot set resalted password for user %q", u.Name()) + } return true } return false @@ -162,3 +168,27 @@ u.doc = udoc return nil } + +func (u *User) Deactivate() error { + if u.doc.Name == AdminUser { + return errors.Unauthorizedf("Can't deactivate admin user") + } + ops := []txn.Op{{ + C: u.st.users.Name, + Id: u.Name(), + Update: bson.D{{"$set", bson.D{{"deactivated", true}}}}, + Assert: txn.DocExists, + }} + if err := u.st.runTransaction(ops); err != nil { + if err == txn.ErrAborted { + err = fmt.Errorf("user no longer exists") + } + return fmt.Errorf("cannot deactivate user %q: %v", u.Name(), err) + } + u.doc.Deactivated = true + return nil +} + +func (u *User) IsDeactivated() bool { + return u.doc.Deactivated +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/user_test.go juju-core-1.17.6/src/launchpad.net/juju-core/state/user_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/user_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/user_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,10 +4,10 @@ package state_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) @@ -158,3 +158,22 @@ c.Assert(u.Name(), gc.Equals, "someuser") c.Assert(u.Tag(), gc.Equals, "user-someuser") } + +func (s *UserSuite) TestDeactivate(c *gc.C) { + u, err := s.State.AddUser("someuser", "") + c.Assert(err, gc.IsNil) + c.Assert(u.IsDeactivated(), gc.Equals, false) + + err = u.Deactivate() + c.Assert(err, gc.IsNil) + c.Assert(u.IsDeactivated(), gc.Equals, true) + c.Assert(u.PasswordValid(""), gc.Equals, false) + +} + +func (s *UserSuite) TestCantDeactivateAdminUser(c *gc.C) { + u, err := s.State.User(state.AdminUser) + c.Assert(err, gc.IsNil) + err = u.Deactivate() + c.Assert(err, gc.ErrorMatches, "Can't deactivate admin user") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/state/watcher.go juju-core-1.17.6/src/launchpad.net/juju-core/state/watcher.go --- juju-core-1.17.4/src/launchpad.net/juju-core/state/watcher.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/state/watcher.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,8 +9,9 @@ "strings" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" "launchpad.net/tomb" "launchpad.net/juju-core/environs/config" @@ -141,7 +142,7 @@ // coll is the collection holding all interesting entities. coll *mgo.Collection // members is used to select the initial set of interesting entities. - members D + members bson.D // filter is used to exclude events not affecting interesting entities. filter func(interface{}) bool // life holds the most recent known life states of interesting entities. @@ -157,7 +158,7 @@ // WatchUnits returns a StringsWatcher that notifies of changes to the // lifecycles of units of s. func (s *Service) WatchUnits() StringsWatcher { - members := D{{"service", s.doc.Name}} + members := bson.D{{"service", s.doc.Name}} prefix := s.doc.Name + "/" filter := func(id interface{}) bool { return strings.HasPrefix(id.(string), prefix) @@ -168,7 +169,7 @@ // WatchRelations returns a StringsWatcher that notifies of changes to the // lifecycles of relations involving s. func (s *Service) WatchRelations() StringsWatcher { - members := D{{"endpoints.servicename", s.doc.Name}} + members := bson.D{{"endpoints.servicename", s.doc.Name}} prefix := s.doc.Name + ":" infix := " " + prefix filter := func(key interface{}) bool { @@ -181,9 +182,9 @@ // WatchEnvironMachines returns a StringsWatcher that notifies of changes to // the lifecycles of the machines (but not containers) in the environment. func (st *State) WatchEnvironMachines() StringsWatcher { - members := D{{"$or", []D{ + members := bson.D{{"$or", []bson.D{ {{"containertype", ""}}, - {{"containertype", D{{"$exists", false}}}}, + {{"containertype", bson.D{{"$exists", false}}}}, }}} filter := func(id interface{}) bool { return !strings.Contains(id.(string), "/") @@ -206,7 +207,7 @@ } func (m *Machine) containersWatcher(isChildRegexp string) StringsWatcher { - members := D{{"_id", D{{"$regex", isChildRegexp}}}} + members := bson.D{{"_id", bson.D{{"$regex", isChildRegexp}}}} compiled := regexp.MustCompile(isChildRegexp) filter := func(key interface{}) bool { return compiled.MatchString(key.(string)) @@ -214,7 +215,7 @@ return newLifecycleWatcher(m.st, m.st.machines, members, filter) } -func newLifecycleWatcher(st *State, coll *mgo.Collection, members D, filter func(key interface{}) bool) StringsWatcher { +func newLifecycleWatcher(st *State, coll *mgo.Collection, members bson.D, filter func(key interface{}) bool) StringsWatcher { w := &lifecycleWatcher{ commonWatcher: commonWatcher{st: st}, coll: coll, @@ -236,7 +237,7 @@ Life Life } -var lifeFields = D{{"_id", 1}, {"life", 1}} +var lifeFields = bson.D{{"_id", 1}, {"life", 1}} // Changes returns the event channel for the LifecycleWatcher. func (w *lifecycleWatcher) Changes() <-chan []string { @@ -276,7 +277,7 @@ // exist are ignored (we'll hear about them in the next set of updates -- // all that's actually happened in that situation is that the watcher // events have lagged a little behind reality). - iter := w.coll.Find(D{{"_id", D{{"$in", changed}}}}).Select(lifeFields).Iter() + iter := w.coll.Find(bson.D{{"_id", bson.D{{"$in", changed}}}}).Select(lifeFields).Iter() var doc lifeDoc for iter.Next(&doc) { latest[doc.Id] = doc.Life @@ -520,7 +521,7 @@ func (w *RelationScopeWatcher) getInitialEvent() (initial *RelationScopeChange, err error) { changes := &RelationScopeChange{} docs := []relationScopeDoc{} - sel := D{{"_id", D{{"$regex", "^" + w.prefix}}}} + sel := bson.D{{"_id", bson.D{{"$regex", "^" + w.prefix}}}} err = w.st.relationScopes.Find(sel).All(&docs) if err != nil { return nil, err @@ -789,7 +790,7 @@ } // lifeWatchFields specifies the fields of a lifeWatchDoc. -var lifeWatchFields = D{{"_id", 1}, {"life", 1}, {"txn-revno", 1}} +var lifeWatchFields = bson.D{{"_id", 1}, {"life", 1}, {"txn-revno", 1}} // initial returns every member of the tracked set. func (w *unitsWatcher) initial() ([]string, error) { @@ -798,7 +799,7 @@ return nil, err } docs := []lifeWatchDoc{} - query := D{{"_id", D{{"$in", initial}}}} + query := bson.D{{"_id", bson.D{{"$in", initial}}}} if err := w.st.units.Find(query).Select(lifeWatchFields).All(&docs); err != nil { return nil, err } @@ -1083,13 +1084,19 @@ return newEntityWatcher(e.st, e.st.environments, e.doc.UUID) } -// WatchForEnvironConfigChanges return a NotifyWatcher waiting for the Environ +// WatchForEnvironConfigChanges returns a NotifyWatcher waiting for the Environ // Config to change. This differs from WatchEnvironConfig in that the watcher // is a NotifyWatcher that does not give content during Changes() func (st *State) WatchForEnvironConfigChanges() NotifyWatcher { return newEntityWatcher(st, st.settings, environGlobalKey) } +// WatchAPIHostPorts returns a NotifyWatcher that notifies +// when the set of API addresses changes. +func (st *State) WatchAPIHostPorts() NotifyWatcher { + return newEntityWatcher(st, st.stateServers, apiHostPortsKey) +} + // WatchConfigSettings returns a watcher for observing changes to the // unit's service configuration settings. The unit must have a charm URL // set before this method is called, and the returned watcher will be @@ -1130,7 +1137,7 @@ doc := &struct { TxnRevno int64 `bson:"txn-revno"` }{} - fields := D{{"txn-revno", 1}} + fields := bson.D{{"txn-revno", 1}} if err := coll.FindId(key).Select(fields).One(doc); err == mgo.ErrNotFound { return -1, nil } else if err != nil { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/channel.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/channel.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/channel.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/channel.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,9 +7,8 @@ "reflect" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" ) // NotifyAsserterC gives helper functions for making assertions about how a diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/charm.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/charm.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/charm.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/charm.go 2014-03-20 12:52:38.000000000 +0000 @@ -128,6 +128,7 @@ type MockCharmStore struct { charms map[string]map[int]*charm.Bundle AuthAttrs string + TestMode bool } func NewMockCharmStore() *MockCharmStore { @@ -139,6 +140,11 @@ return s } +func (s *MockCharmStore) WithTestMode(testMode bool) charm.Repository { + s.TestMode = testMode + return s +} + // SetCharm adds and removes charms in s. The affected charm is identified by // charmURL, which must be revisioned. If bundle is nil, the charm will be // removed; otherwise, it will be stored. It is an error to store a bundle diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/bool.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/bool.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/bool.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/bool.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers - -import ( - "fmt" - "reflect" - - gc "launchpad.net/gocheck" -) - -type isTrueChecker struct { - *gc.CheckerInfo -} - -// IsTrue checks whether a value has an underlying -// boolean type and is true. -var IsTrue gc.Checker = &isTrueChecker{ - &gc.CheckerInfo{Name: "IsTrue", Params: []string{"obtained"}}, -} - -// IsTrue checks whether a value has an underlying -// boolean type and is false. -var IsFalse gc.Checker = gc.Not(IsTrue) - -func (checker *isTrueChecker) Check(params []interface{}, names []string) (result bool, error string) { - - value := reflect.ValueOf(params[0]) - - switch value.Kind() { - case reflect.Bool: - return value.Bool(), "" - } - - return false, fmt.Sprintf("expected type bool, received type %s", value.Type()) -} - -type satisfiesChecker struct { - *gc.CheckerInfo -} - -// Satisfies checks whether a value causes the argument -// function to return true. The function must be of -// type func(T) bool where the value being checked -// is assignable to T. -var Satisfies gc.Checker = &satisfiesChecker{ - &gc.CheckerInfo{ - Name: "Satisfies", - Params: []string{"obtained", "func(T) bool"}, - }, -} - -func (checker *satisfiesChecker) Check(params []interface{}, names []string) (result bool, error string) { - f := reflect.ValueOf(params[1]) - ft := f.Type() - if ft.Kind() != reflect.Func || - ft.NumIn() != 1 || - ft.NumOut() != 1 || - ft.Out(0) != reflect.TypeOf(true) { - return false, fmt.Sprintf("expected func(T) bool, got %s", ft) - } - v := reflect.ValueOf(params[0]) - if !v.IsValid() { - if !canBeNil(ft.In(0)) { - return false, fmt.Sprintf("cannot assign nil to argument %T", ft.In(0)) - } - v = reflect.Zero(ft.In(0)) - } - if !v.Type().AssignableTo(ft.In(0)) { - return false, fmt.Sprintf("wrong argument type %s for %s", v.Type(), ft) - } - return f.Call([]reflect.Value{v})[0].Interface().(bool), "" -} - -func canBeNil(t reflect.Type) bool { - switch t.Kind() { - case reflect.Chan, - reflect.Func, - reflect.Interface, - reflect.Map, - reflect.Ptr, - reflect.Slice: - return true - } - return false -} - -type deepEqualsChecker struct { - *gc.CheckerInfo -} - -// The DeepEquals checker verifies that the obtained value is deep-equal to -// the expected value. The check will work correctly even when facing -// slices, interfaces, and values of different types (which always fail -// the test). -// -// For example: -// -// c.Assert(value, DeepEquals, 42) -// c.Assert(array, DeepEquals, []string{"hi", "there"}) -// -// This checker differs from gocheck.DeepEquals in that -// it will compare a nil slice equal to an empty slice, -// and a nil map equal to an empty map. -var DeepEquals gc.Checker = &deepEqualsChecker{ - &gc.CheckerInfo{Name: "DeepEquals", Params: []string{"obtained", "expected"}}, -} - -func (checker *deepEqualsChecker) Check(params []interface{}, names []string) (result bool, error string) { - if ok, err := DeepEqual(params[0], params[1]); !ok { - return false, err.Error() - } - return true, "" -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/bool_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/bool_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/bool_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/bool_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,121 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers_test - -import ( - "errors" - "os" - - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" -) - -type BoolSuite struct{} - -var _ = gc.Suite(&BoolSuite{}) - -func (s *BoolSuite) TestIsTrue(c *gc.C) { - c.Assert(true, jc.IsTrue) - c.Assert(false, gc.Not(jc.IsTrue)) - - result, msg := jc.IsTrue.Check([]interface{}{false}, nil) - c.Assert(result, gc.Equals, false) - c.Assert(msg, gc.Equals, "") - - result, msg = jc.IsTrue.Check([]interface{}{"foo"}, nil) - c.Assert(result, gc.Equals, false) - c.Check(msg, gc.Equals, `expected type bool, received type string`) - - result, msg = jc.IsTrue.Check([]interface{}{42}, nil) - c.Assert(result, gc.Equals, false) - c.Assert(msg, gc.Equals, `expected type bool, received type int`) -} - -func (s *BoolSuite) TestIsFalse(c *gc.C) { - c.Assert(false, jc.IsFalse) - c.Assert(true, gc.Not(jc.IsFalse)) -} - -func is42(i int) bool { - return i == 42 -} - -var satisfiesTests = []struct { - f interface{} - arg interface{} - result bool - msg string -}{{ - f: is42, - arg: 42, - result: true, -}, { - f: is42, - arg: 41, - result: false, -}, { - f: is42, - arg: "", - result: false, - msg: "wrong argument type string for func(int) bool", -}, { - f: os.IsNotExist, - arg: errors.New("foo"), - result: false, -}, { - f: os.IsNotExist, - arg: os.ErrNotExist, - result: true, -}, { - f: os.IsNotExist, - arg: nil, - result: false, -}, { - f: func(chan int) bool { return true }, - arg: nil, - result: true, -}, { - f: func(func()) bool { return true }, - arg: nil, - result: true, -}, { - f: func(interface{}) bool { return true }, - arg: nil, - result: true, -}, { - f: func(map[string]bool) bool { return true }, - arg: nil, - result: true, -}, { - f: func(*int) bool { return true }, - arg: nil, - result: true, -}, { - f: func([]string) bool { return true }, - arg: nil, - result: true, -}} - -func (s *BoolSuite) TestSatisfies(c *gc.C) { - for i, test := range satisfiesTests { - c.Logf("test %d. %T %T", i, test.f, test.arg) - result, msg := jc.Satisfies.Check([]interface{}{test.arg, test.f}, nil) - c.Check(result, gc.Equals, test.result) - c.Check(msg, gc.Equals, test.msg) - } -} - -func (s *BoolSuite) TestDeepEquals(c *gc.C) { - for i, test := range deepEqualTests { - c.Logf("test %d. %v == %v is %v", i, test.a, test.b, test.eq) - result, msg := jc.DeepEquals.Check([]interface{}{test.a, test.b}, nil) - c.Check(result, gc.Equals, test.eq) - if test.eq { - c.Check(msg, gc.Equals, "") - } else { - c.Check(msg, gc.Not(gc.Equals), "") - } - } -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/checker.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/checker.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/checker.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/checker.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,202 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers - -import ( - "fmt" - "reflect" - "strings" - "time" - - gc "launchpad.net/gocheck" -) - -func TimeBetween(start, end time.Time) gc.Checker { - if end.Before(start) { - return &timeBetweenChecker{end, start} - } - return &timeBetweenChecker{start, end} -} - -type timeBetweenChecker struct { - start, end time.Time -} - -func (checker *timeBetweenChecker) Info() *gc.CheckerInfo { - info := gc.CheckerInfo{ - Name: "TimeBetween", - Params: []string{"obtained"}, - } - return &info -} - -func (checker *timeBetweenChecker) Check(params []interface{}, names []string) (result bool, error string) { - when, ok := params[0].(time.Time) - if !ok { - return false, "obtained value type must be time.Time" - } - if when.Before(checker.start) { - return false, fmt.Sprintf("obtained value %#v type must before start value of %#v", when, checker.start) - } - if when.After(checker.end) { - return false, fmt.Sprintf("obtained value %#v type must after end value of %#v", when, checker.end) - } - return true, "" -} - -// DurationLessThan checker - -type durationLessThanChecker struct { - *gc.CheckerInfo -} - -var DurationLessThan gc.Checker = &durationLessThanChecker{ - &gc.CheckerInfo{Name: "DurationLessThan", Params: []string{"obtained", "expected"}}, -} - -func (checker *durationLessThanChecker) Check(params []interface{}, names []string) (result bool, error string) { - obtained, ok := params[0].(time.Duration) - if !ok { - return false, "obtained value type must be time.Duration" - } - expected, ok := params[1].(time.Duration) - if !ok { - return false, "expected value type must be time.Duration" - } - return obtained.Nanoseconds() < expected.Nanoseconds(), "" -} - -// HasPrefix checker for checking strings - -func stringOrStringer(value interface{}) (string, bool) { - result, isString := value.(string) - if !isString { - if stringer, isStringer := value.(fmt.Stringer); isStringer { - result, isString = stringer.String(), true - } - } - return result, isString -} - -type hasPrefixChecker struct { - *gc.CheckerInfo -} - -var HasPrefix gc.Checker = &hasPrefixChecker{ - &gc.CheckerInfo{Name: "HasPrefix", Params: []string{"obtained", "expected"}}, -} - -func (checker *hasPrefixChecker) Check(params []interface{}, names []string) (result bool, error string) { - expected, ok := params[1].(string) - if !ok { - return false, "expected must be a string" - } - - obtained, isString := stringOrStringer(params[0]) - if isString { - return strings.HasPrefix(obtained, expected), "" - } - - return false, "Obtained value is not a string and has no .String()" -} - -type hasSuffixChecker struct { - *gc.CheckerInfo -} - -var HasSuffix gc.Checker = &hasSuffixChecker{ - &gc.CheckerInfo{Name: "HasSuffix", Params: []string{"obtained", "expected"}}, -} - -func (checker *hasSuffixChecker) Check(params []interface{}, names []string) (result bool, error string) { - expected, ok := params[1].(string) - if !ok { - return false, "expected must be a string" - } - - obtained, isString := stringOrStringer(params[0]) - if isString { - return strings.HasSuffix(obtained, expected), "" - } - - return false, "Obtained value is not a string and has no .String()" -} - -type containsChecker struct { - *gc.CheckerInfo -} - -var Contains gc.Checker = &containsChecker{ - &gc.CheckerInfo{Name: "Contains", Params: []string{"obtained", "expected"}}, -} - -func (checker *containsChecker) Check(params []interface{}, names []string) (result bool, error string) { - expected, ok := params[1].(string) - if !ok { - return false, "expected must be a string" - } - - obtained, isString := stringOrStringer(params[0]) - if isString { - return strings.Contains(obtained, expected), "" - } - - return false, "Obtained value is not a string and has no .String()" -} - -type sameContents struct { - *gc.CheckerInfo -} - -// SameContents checks that the obtained slice contains all the values (and -// same number of values) of the expected slice and vice versa, without respect -// to order or duplicates. Uses DeepEquals on mapped contents to compare. -var SameContents gc.Checker = &sameContents{ - &gc.CheckerInfo{Name: "SameContents", Params: []string{"obtained", "expected"}}, -} - -func (checker *sameContents) Check(params []interface{}, names []string) (result bool, error string) { - if len(params) != 2 { - return false, "SameContents expects two slice arguments" - } - obtained := params[0] - expected := params[1] - - tob := reflect.TypeOf(obtained) - if tob.Kind() != reflect.Slice { - return false, fmt.Sprintf("SameContents expects the obtained value to be a slice, got %q", - tob.Kind()) - } - - texp := reflect.TypeOf(expected) - if texp.Kind() != reflect.Slice { - return false, fmt.Sprintf("SameContents expects the expected value to be a slice, got %q", - texp.Kind()) - } - - if texp != tob { - return false, fmt.Sprintf( - "SameContents expects two slices of the same type, expected: %q, got: %q", - texp, tob) - } - - vexp := reflect.ValueOf(expected) - vob := reflect.ValueOf(obtained) - length := vexp.Len() - - if vob.Len() != length { - // Slice has incorrect number of elements - return false, "" - } - - // spin up maps with the entries as keys and the counts as values - mob := make(map[interface{}]int, length) - mexp := make(map[interface{}]int, length) - - for i := 0; i < length; i++ { - mexp[vexp.Index(i).Interface()]++ - mob[vob.Index(i).Interface()]++ - } - return reflect.DeepEqual(mob, mexp), "" -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/checker_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/checker_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/checker_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/checker_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,120 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers_test - -import ( - "testing" - - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" -) - -func Test(t *testing.T) { gc.TestingT(t) } - -type CheckerSuite struct{} - -var _ = gc.Suite(&CheckerSuite{}) - -func (s *CheckerSuite) TestHasPrefix(c *gc.C) { - c.Assert("foo bar", jc.HasPrefix, "foo") - c.Assert("foo bar", gc.Not(jc.HasPrefix), "omg") -} - -func (s *CheckerSuite) TestHasSuffix(c *gc.C) { - c.Assert("foo bar", jc.HasSuffix, "bar") - c.Assert("foo bar", gc.Not(jc.HasSuffix), "omg") -} - -func (s *CheckerSuite) TestContains(c *gc.C) { - c.Assert("foo bar baz", jc.Contains, "foo") - c.Assert("foo bar baz", jc.Contains, "bar") - c.Assert("foo bar baz", jc.Contains, "baz") - c.Assert("foo bar baz", gc.Not(jc.Contains), "omg") -} - -func (s *CheckerSuite) TestSameContents(c *gc.C) { - //// positive cases //// - - // same - c.Check( - []int{1, 2, 3}, jc.SameContents, - []int{1, 2, 3}) - - // empty - c.Check( - []int{}, jc.SameContents, - []int{}) - - // single - c.Check( - []int{1}, jc.SameContents, - []int{1}) - - // different order - c.Check( - []int{1, 2, 3}, jc.SameContents, - []int{3, 2, 1}) - - // multiple copies of same - c.Check( - []int{1, 1, 2}, jc.SameContents, - []int{2, 1, 1}) - - type test struct { - s string - i int - } - - // test structs - c.Check( - []test{{"a", 1}, {"b", 2}}, jc.SameContents, - []test{{"b", 2}, {"a", 1}}) - - //// negative cases //// - - // different contents - c.Check( - []int{1, 3, 2, 5}, gc.Not(jc.SameContents), - []int{5, 2, 3, 4}) - - // different size slices - c.Check( - []int{1, 2, 3}, gc.Not(jc.SameContents), - []int{1, 2}) - - // different counts of same items - c.Check( - []int{1, 1, 2}, gc.Not(jc.SameContents), - []int{1, 2, 2}) - - /// Error cases /// - // note: for these tests, we can't use gc.Not, since Not passes the error value through - // and checks with a non-empty error always count as failed - // Oddly, there doesn't seem to actually be a way to check for an error from a Checker. - - // different type - res, err := jc.SameContents.Check([]interface{}{ - []string{"1", "2"}, - []int{1, 2}, - }, []string{}) - c.Check(res, jc.IsFalse) - c.Check(err, gc.Not(gc.Equals), "") - - // obtained not a slice - res, err = jc.SameContents.Check([]interface{}{ - "test", - []int{1}, - }, []string{}) - c.Check(res, jc.IsFalse) - c.Check(err, gc.Not(gc.Equals), "") - - // expected not a slice - res, err = jc.SameContents.Check([]interface{}{ - []int{1}, - "test", - }, []string{}) - c.Check(res, jc.IsFalse) - c.Check(err, gc.Not(gc.Equals), "") -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/deepequal.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/deepequal.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/deepequal.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/deepequal.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,303 +0,0 @@ -// Copied with small adaptations from the reflect package in the -// Go source tree. - -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package checkers - -import ( - "fmt" - "reflect" - "unsafe" -) - -// During deepValueEqual, must keep track of checks that are -// in progress. The comparison algorithm assumes that all -// checks in progress are true when it reencounters them. -// Visited comparisons are stored in a map indexed by visit. -type visit struct { - a1 uintptr - a2 uintptr - typ reflect.Type -} - -type mismatchError struct { - v1, v2 reflect.Value - path string - how string -} - -func (err *mismatchError) Error() string { - path := err.path - if path == "" { - path = "top level" - } - return fmt.Sprintf("mismatch at %s: %s; obtained %#v; expected %#v", path, err.how, interfaceOf(err.v1), interfaceOf(err.v2)) -} - -// Tests for deep equality using reflected types. The map argument tracks -// comparisons that have already been seen, which allows short circuiting on -// recursive types. -func deepValueEqual(path string, v1, v2 reflect.Value, visited map[visit]bool, depth int) (ok bool, err error) { - errorf := func(f string, a ...interface{}) error { - return &mismatchError{ - v1: v1, - v2: v2, - path: path, - how: fmt.Sprintf(f, a...), - } - } - if !v1.IsValid() || !v2.IsValid() { - if v1.IsValid() == v2.IsValid() { - return true, nil - } - return false, errorf("validity mismatch") - } - if v1.Type() != v2.Type() { - return false, errorf("type mismatch %s vs %s", v1.Type(), v2.Type()) - } - - // if depth > 10 { panic("deepValueEqual") } // for debugging - hard := func(k reflect.Kind) bool { - switch k { - case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct: - return true - } - return false - } - - if v1.CanAddr() && v2.CanAddr() && hard(v1.Kind()) { - addr1 := v1.UnsafeAddr() - addr2 := v2.UnsafeAddr() - if addr1 > addr2 { - // Canonicalize order to reduce number of entries in visited. - addr1, addr2 = addr2, addr1 - } - - // Short circuit if references are identical ... - if addr1 == addr2 { - return true, nil - } - - // ... or already seen - typ := v1.Type() - v := visit{addr1, addr2, typ} - if visited[v] { - return true, nil - } - - // Remember for later. - visited[v] = true - } - - switch v1.Kind() { - case reflect.Array: - if v1.Len() != v2.Len() { - // can't happen! - return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) - } - for i := 0; i < v1.Len(); i++ { - if ok, err := deepValueEqual( - fmt.Sprintf("%s[%d]", path, i), - v1.Index(i), v2.Index(i), visited, depth+1); !ok { - return false, err - } - } - return true, nil - case reflect.Slice: - // We treat a nil slice the same as an empty slice. - if v1.Len() != v2.Len() { - return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) - } - if v1.Pointer() == v2.Pointer() { - return true, nil - } - for i := 0; i < v1.Len(); i++ { - if ok, err := deepValueEqual( - fmt.Sprintf("%s[%d]", path, i), - v1.Index(i), v2.Index(i), visited, depth+1); !ok { - return false, err - } - } - return true, nil - case reflect.Interface: - if v1.IsNil() || v2.IsNil() { - if v1.IsNil() != v2.IsNil() { - return false, fmt.Errorf("nil vs non-nil interface mismatch") - } - return true, nil - } - return deepValueEqual(path, v1.Elem(), v2.Elem(), visited, depth+1) - case reflect.Ptr: - return deepValueEqual("(*"+path+")", v1.Elem(), v2.Elem(), visited, depth+1) - case reflect.Struct: - for i, n := 0, v1.NumField(); i < n; i++ { - path := path + "." + v1.Type().Field(i).Name - if ok, err := deepValueEqual(path, v1.Field(i), v2.Field(i), visited, depth+1); !ok { - return false, err - } - } - return true, nil - case reflect.Map: - if v1.IsNil() != v2.IsNil() { - return false, errorf("nil vs non-nil mismatch") - } - if v1.Len() != v2.Len() { - return false, errorf("length mismatch, %d vs %d", v1.Len(), v2.Len()) - } - if v1.Pointer() == v2.Pointer() { - return true, nil - } - for _, k := range v1.MapKeys() { - var p string - if k.CanInterface() { - p = path + "[" + fmt.Sprintf("%#v", k.Interface()) + "]" - } else { - p = path + "[someKey]" - } - if ok, err := deepValueEqual(p, v1.MapIndex(k), v2.MapIndex(k), visited, depth+1); !ok { - return false, err - } - } - return true, nil - case reflect.Func: - if v1.IsNil() && v2.IsNil() { - return true, nil - } - // Can't do better than this: - return false, errorf("non-nil functions") - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if v1.Int() != v2.Int() { - return false, errorf("unequal") - } - return true, nil - case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if v1.Uint() != v2.Uint() { - return false, errorf("unequal") - } - return true, nil - case reflect.Float32, reflect.Float64: - if v1.Float() != v2.Float() { - return false, errorf("unequal") - } - return true, nil - case reflect.Complex64, reflect.Complex128: - if v1.Complex() != v2.Complex() { - return false, errorf("unequal") - } - return true, nil - case reflect.Bool: - if v1.Bool() != v2.Bool() { - return false, errorf("unequal") - } - return true, nil - case reflect.String: - if v1.String() != v2.String() { - return false, errorf("unequal") - } - return true, nil - case reflect.Chan, reflect.UnsafePointer: - if v1.Pointer() != v2.Pointer() { - return false, errorf("unequal") - } - return true, nil - default: - panic("unexpected type " + v1.Type().String()) - } -} - -// DeepEqual tests for deep equality. It uses normal == equality where -// possible but will scan elements of arrays, slices, maps, and fields -// of structs. In maps, keys are compared with == but elements use deep -// equality. DeepEqual correctly handles recursive types. Functions are -// equal only if they are both nil. -// -// DeepEqual differs from reflect.DeepEqual in that an empty slice is -// equal to a nil slice. If the two values compare unequal, the -// resulting error holds the first difference encountered. -func DeepEqual(a1, a2 interface{}) (bool, error) { - errorf := func(f string, a ...interface{}) error { - return &mismatchError{ - v1: reflect.ValueOf(a1), - v2: reflect.ValueOf(a2), - path: "", - how: fmt.Sprintf(f, a...), - } - } - if a1 == nil || a2 == nil { - if a1 == a2 { - return true, nil - } - return false, errorf("nil vs non-nil mismatch") - } - v1 := reflect.ValueOf(a1) - v2 := reflect.ValueOf(a2) - if v1.Type() != v2.Type() { - return false, errorf("type mismatch %s vs %s", v1.Type(), v2.Type()) - } - return deepValueEqual("", v1, v2, make(map[visit]bool), 0) -} - -// interfaceOf returns v.Interface() even if v.CanInterface() == false. -// This enables us to call fmt.Printf on a value even if it's derived -// from inside an unexported field. -func interfaceOf(v reflect.Value) interface{} { - if !v.IsValid() { - return nil - } - return bypassCanInterface(v).Interface() -} - -type flag uintptr - -// copied from reflect/value.go -const ( - flagRO flag = 1 << iota -) - -var flagValOffset = func() uintptr { - field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") - if !ok { - panic("reflect.Value has no flag field") - } - return field.Offset -}() - -func flagField(v *reflect.Value) *flag { - return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) -} - -// bypassCanInterface returns a version of v that -// bypasses the CanInterface check. -func bypassCanInterface(v reflect.Value) reflect.Value { - if !v.IsValid() || v.CanInterface() { - return v - } - *flagField(&v) &^= flagRO - return v -} - -// Sanity checks against future reflect package changes -// to the type or semantics of the Value.flag field. -func init() { - field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") - if !ok { - panic("reflect.Value has no flag field") - } - if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { - panic("reflect.Value flag field has changed kind") - } - var t struct { - a int - A int - } - vA := reflect.ValueOf(t).FieldByName("A") - va := reflect.ValueOf(t).FieldByName("a") - flagA := *flagField(&vA) - flaga := *flagField(&va) - if flagA&flagRO != 0 || flaga&flagRO == 0 { - panic("reflect.Value read-only flag has changed value") - } -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/deepequal_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/deepequal_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/deepequal_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/deepequal_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,171 +0,0 @@ -// Copied with small adaptations from the reflect package in the -// Go source tree. We use testing rather than gocheck to preserve -// as much source equivalence as possible. - -// TODO tests for error messages - -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package checkers_test - -import ( - "regexp" - "testing" - - "launchpad.net/juju-core/testing/checkers" -) - -func deepEqual(a1, a2 interface{}) bool { - ok, _ := checkers.DeepEqual(a1, a2) - return ok -} - -type Basic struct { - x int - y float32 -} - -type NotBasic Basic - -type DeepEqualTest struct { - a, b interface{} - eq bool - msg string -} - -// Simple functions for DeepEqual tests. -var ( - fn1 func() // nil. - fn2 func() // nil. - fn3 = func() { fn1() } // Not nil. -) - -var deepEqualTests = []DeepEqualTest{ - // Equalities - {nil, nil, true, ""}, - {1, 1, true, ""}, - {int32(1), int32(1), true, ""}, - {0.5, 0.5, true, ""}, - {float32(0.5), float32(0.5), true, ""}, - {"hello", "hello", true, ""}, - {make([]int, 10), make([]int, 10), true, ""}, - {&[3]int{1, 2, 3}, &[3]int{1, 2, 3}, true, ""}, - {Basic{1, 0.5}, Basic{1, 0.5}, true, ""}, - {error(nil), error(nil), true, ""}, - {map[int]string{1: "one", 2: "two"}, map[int]string{2: "two", 1: "one"}, true, ""}, - {fn1, fn2, true, ""}, - - // Inequalities - {1, 2, false, `mismatch at top level: unequal; obtained 1; expected 2`}, - {int32(1), int32(2), false, `mismatch at top level: unequal; obtained 1; expected 2`}, - {0.5, 0.6, false, `mismatch at top level: unequal; obtained 0\.5; expected 0\.6`}, - {float32(0.5), float32(0.6), false, `mismatch at top level: unequal; obtained 0\.5; expected 0\.6`}, - {"hello", "hey", false, `mismatch at top level: unequal; obtained "hello"; expected "hey"`}, - {make([]int, 10), make([]int, 11), false, `mismatch at top level: length mismatch, 10 vs 11; obtained \[\]int\{0, 0, 0, 0, 0, 0, 0, 0, 0, 0\}; expected \[\]int\{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0\}`}, - {&[3]int{1, 2, 3}, &[3]int{1, 2, 4}, false, `mismatch at \(\*\)\[2\]: unequal; obtained 3; expected 4`}, - {Basic{1, 0.5}, Basic{1, 0.6}, false, `mismatch at \.y: unequal; obtained 0\.5; expected 0\.6`}, - {Basic{1, 0}, Basic{2, 0}, false, `mismatch at \.x: unequal; obtained 1; expected 2`}, - {map[int]string{1: "one", 3: "two"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at \[3\]: validity mismatch; obtained "two"; expected `}, - {map[int]string{1: "one", 2: "txo"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at \[2\]: unequal; obtained "txo"; expected "two"`}, - {map[int]string{1: "one"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at top level: length mismatch, 1 vs 2; obtained map\[int\]string\{1:"one"\}; expected map\[int\]string\{2:"two", 1:"one"\}`}, - {map[int]string{2: "two", 1: "one"}, map[int]string{1: "one"}, false, `mismatch at top level: length mismatch, 2 vs 1; obtained map\[int\]string\{2:"two", 1:"one"\}; expected map\[int\]string\{1:"one"\}`}, - {nil, 1, false, `mismatch at top level: nil vs non-nil mismatch; obtained ; expected 1`}, - {1, nil, false, `mismatch at top level: nil vs non-nil mismatch; obtained 1; expected `}, - {fn1, fn3, false, `mismatch at top level: non-nil functions; obtained \(func\(\)\)\(nil\); expected \(func\(\)\)\(0x[0-9a-f]+\)`}, - {fn3, fn3, false, `mismatch at top level: non-nil functions; obtained \(func\(\)\)\(0x[0-9a-f]+\); expected \(func\(\)\)\(0x[0-9a-f]+\)`}, - - // Nil vs empty: they're the same (difference from normal DeepEqual) - {[]int{}, []int(nil), true, ""}, - {[]int{}, []int{}, true, ""}, - {[]int(nil), []int(nil), true, ""}, - - // Mismatched types - {1, 1.0, false, `mismatch at top level: type mismatch int vs float64; obtained 1; expected 1`}, - {int32(1), int64(1), false, `mismatch at top level: type mismatch int32 vs int64; obtained 1; expected 1`}, - {0.5, "hello", false, `mismatch at top level: type mismatch float64 vs string; obtained 0\.5; expected "hello"`}, - {[]int{1, 2, 3}, [3]int{1, 2, 3}, false, `mismatch at top level: type mismatch \[\]int vs \[3\]int; obtained \[\]int\{1, 2, 3\}; expected \[3\]int\{1, 2, 3\}`}, - {&[3]interface{}{1, 2, 4}, &[3]interface{}{1, 2, "s"}, false, `mismatch at \(\*\)\[2\]: type mismatch int vs string; obtained 4; expected "s"`}, - {Basic{1, 0.5}, NotBasic{1, 0.5}, false, `mismatch at top level: type mismatch checkers_test\.Basic vs checkers_test\.NotBasic; obtained checkers_test\.Basic\{x:1, y:0\.5\}; expected checkers_test\.NotBasic\{x:1, y:0\.5\}`}, - {map[uint]string{1: "one", 2: "two"}, map[int]string{2: "two", 1: "one"}, false, `mismatch at top level: type mismatch map\[uint\]string vs map\[int\]string; obtained map\[uint\]string\{0x1:"one", 0x2:"two"\}; expected map\[int\]string\{2:"two", 1:"one"\}`}, -} - -func TestDeepEqual(t *testing.T) { - for _, test := range deepEqualTests { - r, err := checkers.DeepEqual(test.a, test.b) - if r != test.eq { - t.Errorf("deepEqual(%v, %v) = %v, want %v", test.a, test.b, r, test.eq) - } - if test.eq { - if err != nil { - t.Errorf("deepEqual(%v, %v): unexpected error message %q when equal", test.a, test.b, err) - } - } else { - if ok, _ := regexp.MatchString(test.msg, err.Error()); !ok { - t.Errorf("deepEqual(%v, %v); unexpected error %q, want %q", test.a, test.b, err.Error(), test.msg) - } - } - } -} - -type Recursive struct { - x int - r *Recursive -} - -func TestDeepEqualRecursiveStruct(t *testing.T) { - a, b := new(Recursive), new(Recursive) - *a = Recursive{12, a} - *b = Recursive{12, b} - if !deepEqual(a, b) { - t.Error("deepEqual(recursive same) = false, want true") - } -} - -type _Complex struct { - a int - b [3]*_Complex - c *string - d map[float64]float64 -} - -func TestDeepEqualComplexStruct(t *testing.T) { - m := make(map[float64]float64) - stra, strb := "hello", "hello" - a, b := new(_Complex), new(_Complex) - *a = _Complex{5, [3]*_Complex{a, b, a}, &stra, m} - *b = _Complex{5, [3]*_Complex{b, a, a}, &strb, m} - if !deepEqual(a, b) { - t.Error("deepEqual(complex same) = false, want true") - } -} - -func TestDeepEqualComplexStructInequality(t *testing.T) { - m := make(map[float64]float64) - stra, strb := "hello", "helloo" // Difference is here - a, b := new(_Complex), new(_Complex) - *a = _Complex{5, [3]*_Complex{a, b, a}, &stra, m} - *b = _Complex{5, [3]*_Complex{b, a, a}, &strb, m} - if deepEqual(a, b) { - t.Error("deepEqual(complex different) = true, want false") - } -} - -type UnexpT struct { - m map[int]int -} - -func TestDeepEqualUnexportedMap(t *testing.T) { - // Check that DeepEqual can look at unexported fields. - x1 := UnexpT{map[int]int{1: 2}} - x2 := UnexpT{map[int]int{1: 2}} - if !deepEqual(&x1, &x2) { - t.Error("deepEqual(x1, x2) = false, want true") - } - - y1 := UnexpT{map[int]int{2: 3}} - if deepEqual(&x1, &y1) { - t.Error("deepEqual(x1, y1) = true, want false") - } -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/file.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/file.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/file.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/file.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,154 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers - -import ( - "fmt" - "os" - "reflect" - - gc "launchpad.net/gocheck" -) - -// IsNonEmptyFile checker - -type isNonEmptyFileChecker struct { - *gc.CheckerInfo -} - -var IsNonEmptyFile gc.Checker = &isNonEmptyFileChecker{ - &gc.CheckerInfo{Name: "IsNonEmptyFile", Params: []string{"obtained"}}, -} - -func (checker *isNonEmptyFileChecker) Check(params []interface{}, names []string) (result bool, error string) { - filename, isString := stringOrStringer(params[0]) - if isString { - fileInfo, err := os.Stat(filename) - if os.IsNotExist(err) { - return false, fmt.Sprintf("%s does not exist", filename) - } else if err != nil { - return false, fmt.Sprintf("other stat error: %v", err) - } - if fileInfo.Size() > 0 { - return true, "" - } else { - return false, fmt.Sprintf("%s is empty", filename) - } - } - - value := reflect.ValueOf(params[0]) - return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) -} - -// IsDirectory checker - -type isDirectoryChecker struct { - *gc.CheckerInfo -} - -var IsDirectory gc.Checker = &isDirectoryChecker{ - &gc.CheckerInfo{Name: "IsDirectory", Params: []string{"obtained"}}, -} - -func (checker *isDirectoryChecker) Check(params []interface{}, names []string) (result bool, error string) { - path, isString := stringOrStringer(params[0]) - if isString { - fileInfo, err := os.Stat(path) - if os.IsNotExist(err) { - return false, fmt.Sprintf("%s does not exist", path) - } else if err != nil { - return false, fmt.Sprintf("other stat error: %v", err) - } - if fileInfo.IsDir() { - return true, "" - } else { - return false, fmt.Sprintf("%s is not a directory", path) - } - } - - value := reflect.ValueOf(params[0]) - return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) -} - -// IsSymlink checker - -type isSymlinkChecker struct { - *gc.CheckerInfo -} - -var IsSymlink gc.Checker = &isSymlinkChecker{ - &gc.CheckerInfo{Name: "IsSymlink", Params: []string{"obtained"}}, -} - -func (checker *isSymlinkChecker) Check(params []interface{}, names []string) (result bool, error string) { - path, isString := stringOrStringer(params[0]) - if isString { - fileInfo, err := os.Lstat(path) - if os.IsNotExist(err) { - return false, fmt.Sprintf("%s does not exist", path) - } else if err != nil { - return false, fmt.Sprintf("other stat error: %v", err) - } - if fileInfo.Mode()&os.ModeSymlink != 0 { - return true, "" - } else { - return false, fmt.Sprintf("%s is not a symlink: %+v", path, fileInfo) - } - } - - value := reflect.ValueOf(params[0]) - return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) -} - -// DoesNotExist checker makes sure the path specified doesn't exist. - -type doesNotExistChecker struct { - *gc.CheckerInfo -} - -var DoesNotExist gc.Checker = &doesNotExistChecker{ - &gc.CheckerInfo{Name: "DoesNotExist", Params: []string{"obtained"}}, -} - -func (checker *doesNotExistChecker) Check(params []interface{}, names []string) (result bool, error string) { - path, isString := stringOrStringer(params[0]) - if isString { - _, err := os.Stat(path) - if os.IsNotExist(err) { - return true, "" - } else if err != nil { - return false, fmt.Sprintf("other stat error: %v", err) - } - return false, fmt.Sprintf("%s exists", path) - } - - value := reflect.ValueOf(params[0]) - return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) -} - -// SymlinkDoesNotExist checker makes sure the path specified doesn't exist. - -type symlinkDoesNotExistChecker struct { - *gc.CheckerInfo -} - -var SymlinkDoesNotExist gc.Checker = &symlinkDoesNotExistChecker{ - &gc.CheckerInfo{Name: "SymlinkDoesNotExist", Params: []string{"obtained"}}, -} - -func (checker *symlinkDoesNotExistChecker) Check(params []interface{}, names []string) (result bool, error string) { - path, isString := stringOrStringer(params[0]) - if isString { - _, err := os.Lstat(path) - if os.IsNotExist(err) { - return true, "" - } else if err != nil { - return false, fmt.Sprintf("other stat error: %v", err) - } - return false, fmt.Sprintf("%s exists", path) - } - - value := reflect.ValueOf(params[0]) - return false, fmt.Sprintf("obtained value is not a string and has no .String(), %s:%#v", value.Kind(), params[0]) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/file_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/file_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/file_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/file_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,166 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers_test - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" -) - -type FileSuite struct{} - -var _ = gc.Suite(&FileSuite{}) - -func (s *FileSuite) TestIsNonEmptyFile(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, gc.IsNil) - fmt.Fprintf(file, "something") - file.Close() - - c.Assert(file.Name(), jc.IsNonEmptyFile) -} - -func (s *FileSuite) TestIsNonEmptyFileWithEmptyFile(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, gc.IsNil) - file.Close() - - result, message := jc.IsNonEmptyFile.Check([]interface{}{file.Name()}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, file.Name()+" is empty") -} - -func (s *FileSuite) TestIsNonEmptyFileWithMissingFile(c *gc.C) { - name := filepath.Join(c.MkDir(), "missing") - - result, message := jc.IsNonEmptyFile.Check([]interface{}{name}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, name+" does not exist") -} - -func (s *FileSuite) TestIsNonEmptyFileWithNumber(c *gc.C) { - result, message := jc.IsNonEmptyFile.Check([]interface{}{42}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") -} - -func (s *FileSuite) TestIsDirectory(c *gc.C) { - dir := c.MkDir() - c.Assert(dir, jc.IsDirectory) -} - -func (s *FileSuite) TestIsDirectoryMissing(c *gc.C) { - absentDir := filepath.Join(c.MkDir(), "foo") - - result, message := jc.IsDirectory.Check([]interface{}{absentDir}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, absentDir+" does not exist") -} - -func (s *FileSuite) TestIsDirectoryWithFile(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, gc.IsNil) - file.Close() - - result, message := jc.IsDirectory.Check([]interface{}{file.Name()}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, file.Name()+" is not a directory") -} - -func (s *FileSuite) TestIsDirectoryWithNumber(c *gc.C) { - result, message := jc.IsDirectory.Check([]interface{}{42}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") -} - -func (s *FileSuite) TestDoesNotExist(c *gc.C) { - absentDir := filepath.Join(c.MkDir(), "foo") - c.Assert(absentDir, jc.DoesNotExist) -} - -func (s *FileSuite) TestDoesNotExistWithPath(c *gc.C) { - dir := c.MkDir() - result, message := jc.DoesNotExist.Check([]interface{}{dir}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, dir+" exists") -} - -func (s *FileSuite) TestDoesNotExistWithSymlink(c *gc.C) { - dir := c.MkDir() - deadPath := filepath.Join(dir, "dead") - symlinkPath := filepath.Join(dir, "a-symlink") - err := os.Symlink(deadPath, symlinkPath) - c.Assert(err, gc.IsNil) - // A valid symlink pointing to something that doesn't exist passes. - // Use SymlinkDoesNotExist to check for the non-existence of the link itself. - c.Assert(symlinkPath, jc.DoesNotExist) -} - -func (s *FileSuite) TestDoesNotExistWithNumber(c *gc.C) { - result, message := jc.DoesNotExist.Check([]interface{}{42}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") -} - -func (s *FileSuite) TestSymlinkDoesNotExist(c *gc.C) { - absentDir := filepath.Join(c.MkDir(), "foo") - c.Assert(absentDir, jc.SymlinkDoesNotExist) -} - -func (s *FileSuite) TestSymlinkDoesNotExistWithPath(c *gc.C) { - dir := c.MkDir() - result, message := jc.SymlinkDoesNotExist.Check([]interface{}{dir}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, dir+" exists") -} - -func (s *FileSuite) TestSymlinkDoesNotExistWithSymlink(c *gc.C) { - dir := c.MkDir() - deadPath := filepath.Join(dir, "dead") - symlinkPath := filepath.Join(dir, "a-symlink") - err := os.Symlink(deadPath, symlinkPath) - c.Assert(err, gc.IsNil) - - result, message := jc.SymlinkDoesNotExist.Check([]interface{}{symlinkPath}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, symlinkPath+" exists") -} - -func (s *FileSuite) TestSymlinkDoesNotExistWithNumber(c *gc.C) { - result, message := jc.SymlinkDoesNotExist.Check([]interface{}{42}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, gc.Equals, "obtained value is not a string and has no .String(), int:42") -} - -func (s *FileSuite) TestIsSymlink(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, gc.IsNil) - c.Log(file.Name()) - c.Log(filepath.Dir(file.Name())) - symlinkPath := filepath.Join(filepath.Dir(file.Name()), "a-symlink") - err = os.Symlink(file.Name(), symlinkPath) - c.Assert(err, gc.IsNil) - - c.Assert(symlinkPath, jc.IsSymlink) -} - -func (s *FileSuite) TestIsSymlinkWithFile(c *gc.C) { - file, err := ioutil.TempFile(c.MkDir(), "") - c.Assert(err, gc.IsNil) - result, message := jc.IsSymlink.Check([]interface{}{file.Name()}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, jc.Contains, " is not a symlink") -} - -func (s *FileSuite) TestIsSymlinkWithDir(c *gc.C) { - result, message := jc.IsSymlink.Check([]interface{}{c.MkDir()}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(message, jc.Contains, " is not a symlink") -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/log.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/log.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/log.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/log.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,109 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers - -import ( - "fmt" - "regexp" - "strings" - - "github.com/loggo/loggo" - gc "launchpad.net/gocheck" -) - -type SimpleMessage struct { - Level loggo.Level - Message string -} - -type SimpleMessages []SimpleMessage - -func (s SimpleMessage) String() string { - return fmt.Sprintf("%s %s", s.Level, s.Message) -} - -func (s SimpleMessages) GoString() string { - out := make([]string, len(s)) - for i, m := range s { - out[i] = m.String() - } - return fmt.Sprintf("SimpleMessages{\n%s\n}", strings.Join(out, "\n")) -} - -func logToSimpleMessages(log []loggo.TestLogValues) SimpleMessages { - out := make(SimpleMessages, len(log)) - for i, val := range log { - out[i].Level = val.Level - out[i].Message = val.Message - } - return out -} - -type logMatches struct { - *gc.CheckerInfo -} - -func (checker *logMatches) Check(params []interface{}, names []string) (result bool, error string) { - var obtained SimpleMessages - switch params[0].(type) { - case []loggo.TestLogValues: - obtained = logToSimpleMessages(params[0].([]loggo.TestLogValues)) - default: - return false, "Obtained value must be of type []loggo.TestLogValues or SimpleMessage" - } - - var expected SimpleMessages - switch param := params[1].(type) { - case []SimpleMessage: - expected = SimpleMessages(param) - case SimpleMessages: - expected = param - case []string: - expected = make(SimpleMessages, len(param)) - for i, s := range param { - expected[i] = SimpleMessage{ - Message: s, - Level: loggo.UNSPECIFIED, - } - } - default: - return false, "Expected value must be of type []string or []SimpleMessage" - } - - obtainedSinceLastMatch := obtained - for len(expected) > 0 && len(obtained) >= len(expected) { - var msg SimpleMessage - msg, obtained = obtained[0], obtained[1:] - expect := expected[0] - if expect.Level != loggo.UNSPECIFIED && msg.Level != expect.Level { - continue - } - matched, err := regexp.MatchString(expect.Message, msg.Message) - if err != nil { - return false, fmt.Sprintf("bad message regexp %q: %v", expect.Message, err) - } else if !matched { - continue - } - expected = expected[1:] - obtainedSinceLastMatch = obtained - } - if len(obtained) < len(expected) { - params[0] = obtainedSinceLastMatch - params[1] = expected - return false, "" - } - return true, "" -} - -// LogMatches checks whether a given TestLogValues actually contains the log -// messages we expected. If you compare it against a list of strings, we only -// compare that the strings in the messages are correct. You can alternatively -// pass a slice of SimpleMessage and we will check that the log levels are -// also correct. -// -// The log may contain additional messages before and after each of the specified -// expected messages. -var LogMatches gc.Checker = &logMatches{ - &gc.CheckerInfo{Name: "LogMatches", Params: []string{"obtained", "expected"}}, -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/log_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/log_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/log_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/log_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,112 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers_test - -import ( - "github.com/loggo/loggo" - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" -) - -type LogMatchesSuite struct{} - -var _ = gc.Suite(&LogMatchesSuite{}) - -func (s *LogMatchesSuite) TestMatchSimpleMessage(c *gc.C) { - log := []loggo.TestLogValues{ - {Level: loggo.INFO, Message: "foo bar"}, - {Level: loggo.INFO, Message: "12345"}, - } - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.INFO, "foo bar"}, - {loggo.INFO, "12345"}, - }) - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.INFO, "foo .*"}, - {loggo.INFO, "12345"}, - }) - // UNSPECIFIED means we don't care what the level is, - // just check the message string matches. - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.UNSPECIFIED, "foo .*"}, - {loggo.INFO, "12345"}, - }) - c.Check(log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ - {loggo.INFO, "foo bar"}, - {loggo.DEBUG, "12345"}, - }) -} - -func (s *LogMatchesSuite) TestMatchStrings(c *gc.C) { - log := []loggo.TestLogValues{ - {Level: loggo.INFO, Message: "foo bar"}, - {Level: loggo.INFO, Message: "12345"}, - } - c.Check(log, jc.LogMatches, []string{"foo bar", "12345"}) - c.Check(log, jc.LogMatches, []string{"foo .*", "12345"}) - c.Check(log, gc.Not(jc.LogMatches), []string{"baz", "bing"}) -} - -func (s *LogMatchesSuite) TestMatchInexact(c *gc.C) { - log := []loggo.TestLogValues{ - {Level: loggo.INFO, Message: "foo bar"}, - {Level: loggo.INFO, Message: "baz"}, - {Level: loggo.DEBUG, Message: "12345"}, - {Level: loggo.ERROR, Message: "12345"}, - {Level: loggo.INFO, Message: "67890"}, - } - c.Check(log, jc.LogMatches, []string{"foo bar", "12345"}) - c.Check(log, jc.LogMatches, []string{"foo .*", "12345"}) - c.Check(log, jc.LogMatches, []string{"foo .*", "67890"}) - c.Check(log, jc.LogMatches, []string{"67890"}) - - // Matches are always left-most after the previous match. - c.Check(log, jc.LogMatches, []string{".*", "baz"}) - c.Check(log, jc.LogMatches, []string{"foo bar", ".*", "12345"}) - c.Check(log, jc.LogMatches, []string{"foo bar", ".*", "67890"}) - - // Order is important: 67890 advances to the last item in obtained, - // and so there's nothing after to match against ".*". - c.Check(log, gc.Not(jc.LogMatches), []string{"67890", ".*"}) - // ALL specified patterns MUST match in the order given. - c.Check(log, gc.Not(jc.LogMatches), []string{".*", "foo bar"}) - - // Check that levels are matched. - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.UNSPECIFIED, "12345"}, - {loggo.UNSPECIFIED, "12345"}, - }) - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.DEBUG, "12345"}, - {loggo.ERROR, "12345"}, - }) - c.Check(log, jc.LogMatches, []jc.SimpleMessage{ - {loggo.DEBUG, "12345"}, - {loggo.INFO, ".*"}, - }) - c.Check(log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ - {loggo.DEBUG, "12345"}, - {loggo.INFO, ".*"}, - {loggo.UNSPECIFIED, ".*"}, - }) -} - -func (s *LogMatchesSuite) TestFromLogMatches(c *gc.C) { - tw := &loggo.TestWriter{} - _, err := loggo.ReplaceDefaultWriter(tw) - c.Assert(err, gc.IsNil) - defer loggo.ResetWriters() - logger := loggo.GetLogger("test") - logger.SetLogLevel(loggo.DEBUG) - logger.Infof("foo") - logger.Debugf("bar") - logger.Tracef("hidden") - c.Check(tw.Log, jc.LogMatches, []string{"foo", "bar"}) - c.Check(tw.Log, gc.Not(jc.LogMatches), []string{"foo", "bad"}) - c.Check(tw.Log, gc.Not(jc.LogMatches), []jc.SimpleMessage{ - {loggo.INFO, "foo"}, - {loggo.INFO, "bar"}, - }) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/relop.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/relop.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/relop.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/relop.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,93 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers - -import ( - "fmt" - "reflect" - - gc "launchpad.net/gocheck" -) - -// GreaterThan checker - -type greaterThanChecker struct { - *gc.CheckerInfo -} - -var GreaterThan gc.Checker = &greaterThanChecker{ - &gc.CheckerInfo{Name: "GreaterThan", Params: []string{"obtained", "expected"}}, -} - -func (checker *greaterThanChecker) Check(params []interface{}, names []string) (result bool, error string) { - defer func() { - if v := recover(); v != nil { - result = false - error = fmt.Sprint(v) - } - }() - - p0value := reflect.ValueOf(params[0]) - p1value := reflect.ValueOf(params[1]) - switch p0value.Kind() { - case reflect.Int, - reflect.Int8, - reflect.Int16, - reflect.Int32, - reflect.Int64: - return p0value.Int() > p1value.Int(), "" - case reflect.Uint, - reflect.Uint8, - reflect.Uint16, - reflect.Uint32, - reflect.Uint64: - return p0value.Uint() > p1value.Uint(), "" - case reflect.Float32, - reflect.Float64: - return p0value.Float() > p1value.Float(), "" - default: - } - return false, fmt.Sprintf("obtained value %s:%#v not supported", p0value.Kind(), params[0]) -} - -// LessThan checker - -type lessThanChecker struct { - *gc.CheckerInfo -} - -var LessThan gc.Checker = &lessThanChecker{ - &gc.CheckerInfo{Name: "LessThan", Params: []string{"obtained", "expected"}}, -} - -func (checker *lessThanChecker) Check(params []interface{}, names []string) (result bool, error string) { - defer func() { - if v := recover(); v != nil { - result = false - error = fmt.Sprint(v) - } - }() - - p0value := reflect.ValueOf(params[0]) - p1value := reflect.ValueOf(params[1]) - switch p0value.Kind() { - case reflect.Int, - reflect.Int8, - reflect.Int16, - reflect.Int32, - reflect.Int64: - return p0value.Int() < p1value.Int(), "" - case reflect.Uint, - reflect.Uint8, - reflect.Uint16, - reflect.Uint32, - reflect.Uint64: - return p0value.Uint() < p1value.Uint(), "" - case reflect.Float32, - reflect.Float64: - return p0value.Float() < p1value.Float(), "" - default: - } - return false, fmt.Sprintf("obtained value %s:%#v not supported", p0value.Kind(), params[0]) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/relop_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/relop_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/checkers/relop_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/checkers/relop_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package checkers_test - -import ( - gc "launchpad.net/gocheck" - - jc "launchpad.net/juju-core/testing/checkers" -) - -type RelopSuite struct{} - -var _ = gc.Suite(&RelopSuite{}) - -func (s *RelopSuite) TestGreaterThan(c *gc.C) { - c.Assert(45, jc.GreaterThan, 42) - c.Assert(2.25, jc.GreaterThan, 1.0) - c.Assert(42, gc.Not(jc.GreaterThan), 42) - c.Assert(10, gc.Not(jc.GreaterThan), 42) - - result, msg := jc.GreaterThan.Check([]interface{}{"Hello", "World"}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(msg, gc.Equals, `obtained value string:"Hello" not supported`) -} - -func (s *RelopSuite) TestLessThan(c *gc.C) { - c.Assert(42, jc.LessThan, 45) - c.Assert(1.0, jc.LessThan, 2.25) - c.Assert(42, gc.Not(jc.LessThan), 42) - c.Assert(42, gc.Not(jc.LessThan), 10) - - result, msg := jc.LessThan.Check([]interface{}{"Hello", "World"}, nil) - c.Assert(result, jc.IsFalse) - c.Assert(msg, gc.Equals, `obtained value string:"Hello" not supported`) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/mgo.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/mgo.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/mgo.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/mgo.go 2014-03-20 12:52:38.000000000 +0000 @@ -19,6 +19,7 @@ stdtesting "testing" "time" + "github.com/juju/loggo" "labix.org/v2/mgo" gc "launchpad.net/gocheck" @@ -30,6 +31,7 @@ var ( // MgoServer is a shared mongo server used by tests. MgoServer = &MgoInstance{ssl: true} + logger = loggo.GetLogger("juju.testing") ) type MgoInstance struct { @@ -83,6 +85,7 @@ if err != nil { return err } + logger.Debugf("starting mongo in %s", dbdir) // give them all the same keyfile so they can talk appropriately keyFilePath := filepath.Join(dbdir, "keyfile") @@ -106,6 +109,7 @@ os.RemoveAll(inst.dir) inst.dir = "" } + logger.Debugf("started mongod pid %d in %s on port %d", inst.server.Process.Pid, dbdir, inst.port) return err } @@ -148,14 +152,15 @@ started := make(chan struct{}) go func() { <-started - lines := readLines(out, 20) + lines := readLines(fmt.Sprintf("mongod:%v", mgoport), out, 20) err := server.Wait() exitErr, _ := err.(*exec.ExitError) if err == nil || exitErr != nil && exitErr.Exited() { // mongodb has exited without being killed, so print the // last few lines of its log output. + log.Errorf("mongodb has exited without being killed") for _, line := range lines { - log.Infof("mongod: %s", line) + log.Errorf("mongod: %s", line) } } close(exited) @@ -180,6 +185,7 @@ func (inst *MgoInstance) Destroy() { if inst.server != nil { + logger.Debugf("killing mongod pid %d in %s on port %d", inst.server.Process.Pid, inst.dir, inst.port) inst.kill() os.RemoveAll(inst.dir) inst.addr, inst.dir = "", "" @@ -189,6 +195,7 @@ // Restart restarts the mongo server, useful for // testing what happens when a state server goes down. func (inst *MgoInstance) Restart() { + logger.Debugf("restarting mongod pid %d in %s on port %d", inst.server.Process.Pid, inst.dir, inst.port) inst.kill() if err := inst.Start(inst.ssl); err != nil { panic(err) @@ -220,13 +227,14 @@ // readLines reads lines from the given reader and returns // the last n non-empty lines, ignoring empty lines. -func readLines(r io.Reader, n int) []string { +func readLines(prefix string, r io.Reader, n int) []string { br := bufio.NewReader(r) lines := make([]string, n) i := 0 for { line, err := br.ReadString('\n') if line = strings.TrimRight(line, "\n"); line != "" { + logger.Tracef("%s: %s", prefix, line) lines[i%n] = line i++ } @@ -253,7 +261,7 @@ // MustDial returns a new connection to the MongoDB server, and panics on // errors. func (inst *MgoInstance) MustDial() *mgo.Session { - s, err := inst.dial(false) + s, err := mgo.DialWithInfo(inst.DialInfo()) if err != nil { panic(err) } @@ -262,45 +270,59 @@ // Dial returns a new connection to the MongoDB server. func (inst *MgoInstance) Dial() (*mgo.Session, error) { - return inst.dial(false) + return mgo.DialWithInfo(inst.DialInfo()) +} + +// DialInfo returns information suitable for dialling the +// receiving MongoDB instance. +func (inst *MgoInstance) DialInfo() *mgo.DialInfo { + return MgoDialInfo(inst.addr) } // DialDirect returns a new direct connection to the shared MongoDB server. This // must be used if you're connecting to a replicaset that hasn't been initiated // yet. func (inst *MgoInstance) DialDirect() (*mgo.Session, error) { - return inst.dial(true) + info := inst.DialInfo() + info.Direct = true + return mgo.DialWithInfo(info) } // MustDialDirect works like DialDirect, but panics on errors. func (inst *MgoInstance) MustDialDirect() *mgo.Session { - session, err := inst.dial(true) + session, err := inst.DialDirect() if err != nil { panic(err) } return session } -func (inst *MgoInstance) dial(direct bool) (*mgo.Session, error) { +// MgoDialInfo returns a DialInfo suitable +// for dialling an MgoInstance at any of the +// given addresses. +func MgoDialInfo(addrs ...string) *mgo.DialInfo { pool := x509.NewCertPool() xcert, err := cert.ParseCert([]byte(CACert)) if err != nil { - return nil, err + panic(err) } pool.AddCert(xcert) tlsConfig := &tls.Config{ RootCAs: pool, ServerName: "anything", } - session, err := mgo.DialWithInfo(&mgo.DialInfo{ - Direct: direct, - Addrs: []string{inst.addr}, + return &mgo.DialInfo{ + Addrs: addrs, Dial: func(addr net.Addr) (net.Conn, error) { - return tls.Dial("tcp", addr.String(), tlsConfig) + conn, err := tls.Dial("tcp", addr.String(), tlsConfig) + if err != nil { + logger.Debugf("tls.Dial(%s) failed with %v", addr, err) + return nil, err + } + return conn, nil }, Timeout: mgoDialTimeout, - }) - return session, err + } } func (s *MgoSuite) SetUpTest(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cleanup.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cleanup.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cleanup.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cleanup.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,92 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase - -import ( - "os/exec" - - gc "launchpad.net/gocheck" -) - -type CleanupFunc func(*gc.C) -type cleanupStack []CleanupFunc - -// CleanupSuite adds the ability to add cleanup functions that are called -// during either test tear down or suite tear down depending on the method -// called. -type CleanupSuite struct { - testStack cleanupStack - suiteStack cleanupStack -} - -func (s *CleanupSuite) SetUpSuite(c *gc.C) { - s.suiteStack = nil -} - -func (s *CleanupSuite) TearDownSuite(c *gc.C) { - s.callStack(c, s.suiteStack) -} - -func (s *CleanupSuite) SetUpTest(c *gc.C) { - s.testStack = nil -} - -func (s *CleanupSuite) TearDownTest(c *gc.C) { - s.callStack(c, s.testStack) -} - -func (s *CleanupSuite) callStack(c *gc.C, stack cleanupStack) { - for i := len(stack) - 1; i >= 0; i-- { - stack[i](c) - } -} - -// AddCleanup pushes the cleanup function onto the stack of functions to be -// called during TearDownTest. -func (s *CleanupSuite) AddCleanup(cleanup CleanupFunc) { - s.testStack = append(s.testStack, cleanup) -} - -// AddSuiteCleanup pushes the cleanup function onto the stack of functions to -// be called during TearDownSuite. -func (s *CleanupSuite) AddSuiteCleanup(cleanup CleanupFunc) { - s.suiteStack = append(s.suiteStack, cleanup) -} - -// PatchEnvironment sets the environment variable 'name' the the value passed -// in. The old value is saved and returned to the original value at test tear -// down time using a cleanup function. -func (s *CleanupSuite) PatchEnvironment(name, value string) { - restore := PatchEnvironment(name, value) - s.AddCleanup(func(*gc.C) { restore() }) -} - -// PatchEnvPathPrepend prepends the given path to the environment $PATH and restores the -// original path on test teardown. -func (s *CleanupSuite) PatchEnvPathPrepend(dir string) { - restore := PatchEnvPathPrepend(dir) - s.AddCleanup(func(*gc.C) { restore() }) -} - -// PatchValue sets the 'dest' variable the the value passed in. The old value -// is saved and returned to the original value at test tear down time using a -// cleanup function. The value must be assignable to the element type of the -// destination. -func (s *CleanupSuite) PatchValue(dest, value interface{}) { - restore := PatchValue(dest, value) - s.AddCleanup(func(*gc.C) { restore() }) -} - -// HookCommandOutput calls the package function of the same name to mock out -// the result of a particular comand execution, and will call the restore -// function on test teardown. -func (s *CleanupSuite) HookCommandOutput( - outputFunc *func(cmd *exec.Cmd) ([]byte, error), - output []byte, - err error, -) <-chan *exec.Cmd { - result, restore := HookCommandOutput(outputFunc, output, err) - s.AddCleanup(func(*gc.C) { restore() }) - return result -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cleanup_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cleanup_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cleanup_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cleanup_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase_test - -import ( - "os" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" -) - -type cleanupSuite struct { - testbase.CleanupSuite -} - -var _ = gc.Suite(&cleanupSuite{}) - -func (s *cleanupSuite) TestTearDownSuiteEmpty(c *gc.C) { - // The suite stack is empty initially, check we can tear that down. - s.TearDownSuite(c) - s.SetUpSuite(c) -} - -func (s *cleanupSuite) TestTearDownTestEmpty(c *gc.C) { - // The test stack is empty initially, check we can tear that down. - s.TearDownTest(c) - s.SetUpTest(c) -} - -func (s *cleanupSuite) TestAddSuiteCleanup(c *gc.C) { - order := []string{} - s.AddSuiteCleanup(func(*gc.C) { - order = append(order, "first") - }) - s.AddSuiteCleanup(func(*gc.C) { - order = append(order, "second") - }) - - s.TearDownSuite(c) - c.Assert(order, gc.DeepEquals, []string{"second", "first"}) - - // SetUpSuite resets the cleanup stack, this stops the cleanup functions - // being called again. - s.SetUpSuite(c) -} - -func (s *cleanupSuite) TestAddCleanup(c *gc.C) { - order := []string{} - s.AddCleanup(func(*gc.C) { - order = append(order, "first") - }) - s.AddCleanup(func(*gc.C) { - order = append(order, "second") - }) - - s.TearDownTest(c) - c.Assert(order, gc.DeepEquals, []string{"second", "first"}) - - // SetUpTest resets the cleanup stack, this stops the cleanup functions - // being called again. - s.SetUpTest(c) -} - -func (s *cleanupSuite) TestPatchEnvironment(c *gc.C) { - const envName = "TESTING_PATCH_ENVIRONMENT" - // remember the old value, and set it to something we can check - oldValue := os.Getenv(envName) - os.Setenv(envName, "initial") - - s.PatchEnvironment(envName, "new value") - // Using check to make sure the environment gets set back properly in the test. - c.Check(os.Getenv(envName), gc.Equals, "new value") - - s.TearDownTest(c) - c.Check(os.Getenv(envName), gc.Equals, "initial") - - // SetUpTest resets the cleanup stack, this stops the cleanup functions - // being called again. - s.SetUpTest(c) - // explicitly return the envName to the old value - os.Setenv(envName, oldValue) -} - -func (s *cleanupSuite) TestPatchValueInt(c *gc.C) { - i := 42 - s.PatchValue(&i, 0) - c.Assert(i, gc.Equals, 0) - - s.TearDownTest(c) - c.Assert(i, gc.Equals, 42) - - // SetUpTest resets the cleanup stack, this stops the cleanup functions - // being called again. - s.SetUpTest(c) -} - -func (s *cleanupSuite) TestPatchValueFunction(c *gc.C) { - function := func() string { - return "original" - } - - s.PatchValue(&function, func() string { - return "patched" - }) - c.Assert(function(), gc.Equals, "patched") - - s.TearDownTest(c) - c.Assert(function(), gc.Equals, "original") - - // SetUpTest resets the cleanup stack, this stops the cleanup functions - // being called again. - s.SetUpTest(c) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cmd.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cmd.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cmd.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cmd.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase - -import ( - "os/exec" -) - -// HookCommandOutput intercepts CommandOutput to a function that passes the -// actual command and it's output back via a channel, and returns the error -// passed into this function. It also returns a cleanup function so you can -// restore the original function -func HookCommandOutput( - outputFunc *func(cmd *exec.Cmd) ([]byte, error), output []byte, err error) (<-chan *exec.Cmd, func()) { - - cmdChan := make(chan *exec.Cmd, 1) - origCommandOutput := *outputFunc - cleanup := func() { - *outputFunc = origCommandOutput - } - *outputFunc = func(cmd *exec.Cmd) ([]byte, error) { - cmdChan <- cmd - return output, err - } - return cmdChan, cleanup -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cmd_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cmd_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/cmd_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/cmd_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase_test - -import ( - "os/exec" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" -) - -type CmdSuite struct { - testbase.LoggingSuite -} - -var _ = gc.Suite(&CmdSuite{}) - -func (s *CmdSuite) TestHookCommandOutput(c *gc.C) { - var CommandOutput = (*exec.Cmd).CombinedOutput - - cmdChan, cleanup := testbase.HookCommandOutput(&CommandOutput, []byte{1, 2, 3, 4}, nil) - defer cleanup() - - testCmd := exec.Command("fake-command", "arg1", "arg2") - out, err := CommandOutput(testCmd) - c.Assert(err, gc.IsNil) - cmd := <-cmdChan - c.Assert(out, gc.DeepEquals, []byte{1, 2, 3, 4}) - c.Assert(cmd.Args, gc.DeepEquals, []string{"fake-command", "arg1", "arg2"}) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/imports.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/imports.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/imports.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/imports.go 2014-03-20 12:52:38.000000000 +0000 @@ -18,16 +18,13 @@ // imported by the packageName parameter. The resulting list removes the // common prefix "launchpad.net/juju-core/" leaving just the short names. func FindJujuCoreImports(c *gc.C, packageName string) []string { - var result []string allpkgs := make(map[string]bool) findJujuCoreImports(c, packageName, allpkgs) - for name := range allpkgs { result = append(result, name[len(jujuPkgPrefix):]) } - sort.Strings(result) return result } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/log.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/log.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/log.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/log.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,47 +4,36 @@ package testbase import ( - "fmt" - "time" + "flag" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "github.com/juju/testing/logging" gc "launchpad.net/gocheck" ) // LoggingSuite redirects the juju logger to the test logger // when embedded in a gocheck suite type. type LoggingSuite struct { - CleanupSuite -} - -type gocheckWriter struct { - c *gc.C -} - -func (w *gocheckWriter) Write(level loggo.Level, module, filename string, line int, timestamp time.Time, message string) { - // Magic calldepth value... - w.c.Output(3, fmt.Sprintf("%s %s %s", level, module, message)) + logging.LoggingSuite } func (t *LoggingSuite) SetUpSuite(c *gc.C) { - t.CleanupSuite.SetUpSuite(c) + t.LoggingSuite.SetUpSuite(c) t.setUp(c) - t.AddSuiteCleanup(func(*gc.C) { - loggo.ResetLoggers() - loggo.ResetWriters() - }) } func (t *LoggingSuite) SetUpTest(c *gc.C) { - t.CleanupSuite.SetUpTest(c) + t.LoggingSuite.SetUpTest(c) t.PatchEnvironment("JUJU_LOGGING_CONFIG", "") t.setUp(c) } +var logConfig = flag.String("juju.log", "DEBUG", "logging configuration (see http://godoc.org/github.com/juju/loggo#ConfigureLoggers; also accepts a bare log level to configure the log level of the root module") + func (t *LoggingSuite) setUp(c *gc.C) { - loggo.ResetWriters() - loggo.ReplaceDefaultWriter(&gocheckWriter{c}) - loggo.ResetLoggers() - loggo.GetLogger("juju").SetLogLevel(loggo.DEBUG) - loggo.GetLogger("unit").SetLogLevel(loggo.DEBUG) + if _, ok := loggo.ParseLevel(*logConfig); ok { + *logConfig = "=" + *logConfig + } + err := loggo.ConfigureLoggers(*logConfig) + c.Assert(err, gc.IsNil) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/patch.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/patch.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/patch.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/patch.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase - -import ( - "os" - "reflect" - "strings" -) - -// Restorer holds a function that can be used -// to restore some previous state. -type Restorer func() - -// Add returns a Restorer that restores first f1 -// and then f. It is valid to call this on a nil -// Restorer. -func (f Restorer) Add(f1 Restorer) Restorer { - return func() { - f1.Restore() - if f != nil { - f.Restore() - } - } -} - -// Restore restores some previous state. -func (r Restorer) Restore() { - r() -} - -// PatchValue sets the value pointed to by the given destination to the given -// value, and returns a function to restore it to its original value. The -// value must be assignable to the element type of the destination. -func PatchValue(dest, value interface{}) Restorer { - destv := reflect.ValueOf(dest).Elem() - oldv := reflect.New(destv.Type()).Elem() - oldv.Set(destv) - valuev := reflect.ValueOf(value) - if !valuev.IsValid() { - // This isn't quite right when the destination type is not - // nilable, but it's better than the complex alternative. - valuev = reflect.Zero(destv.Type()) - } - destv.Set(valuev) - return func() { - destv.Set(oldv) - } -} - -// PatchEnvironment provides a test a simple way to override a single -// environment variable. A function is returned that will return the -// environment to what it was before. -func PatchEnvironment(name, value string) Restorer { - oldValue := os.Getenv(name) - os.Setenv(name, value) - return func() { - os.Setenv(name, oldValue) - } -} - -// PatchEnvPathPrepend provides a simple way to prepend path to the start of the -// PATH environment variable. Returns a function that restores the environment -// to what it was before. -func PatchEnvPathPrepend(dir string) Restorer { - return PatchEnvironment("PATH", joinPathLists(dir, os.Getenv("PATH"))) -} - -func joinPathLists(paths ...string) string { - return strings.Join(paths, string(os.PathListSeparator)) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/patch_test.go juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/patch_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/testing/testbase/patch_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/testing/testbase/patch_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,100 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testbase_test - -import ( - "errors" - "os" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" -) - -type PatchValueSuite struct{} - -var _ = gc.Suite(&PatchValueSuite{}) - -func (*PatchValueSuite) TestSetInt(c *gc.C) { - i := 99 - restore := testbase.PatchValue(&i, 88) - c.Assert(i, gc.Equals, 88) - restore() - c.Assert(i, gc.Equals, 99) -} - -func (*PatchValueSuite) TestSetError(c *gc.C) { - oldErr := errors.New("foo") - newErr := errors.New("bar") - err := oldErr - restore := testbase.PatchValue(&err, newErr) - c.Assert(err, gc.Equals, newErr) - restore() - c.Assert(err, gc.Equals, oldErr) -} - -func (*PatchValueSuite) TestSetErrorToNil(c *gc.C) { - oldErr := errors.New("foo") - err := oldErr - restore := testbase.PatchValue(&err, nil) - c.Assert(err, gc.Equals, nil) - restore() - c.Assert(err, gc.Equals, oldErr) -} - -func (*PatchValueSuite) TestSetMapToNil(c *gc.C) { - oldMap := map[string]int{"foo": 1234} - m := oldMap - restore := testbase.PatchValue(&m, nil) - c.Assert(m, gc.IsNil) - restore() - c.Assert(m, gc.DeepEquals, oldMap) -} - -func (*PatchValueSuite) TestSetPanicsWhenNotAssignable(c *gc.C) { - i := 99 - type otherInt int - c.Assert(func() { testbase.PatchValue(&i, otherInt(88)) }, gc.PanicMatches, `reflect\.Set: value of type testbase_test\.otherInt is not assignable to type int`) -} - -type PatchEnvironmentSuite struct{} - -var _ = gc.Suite(&PatchEnvironmentSuite{}) - -func (*PatchEnvironmentSuite) TestPatchEnvironment(c *gc.C) { - const envName = "TESTING_PATCH_ENVIRONMENT" - // remember the old value, and set it to something we can check - oldValue := os.Getenv(envName) - os.Setenv(envName, "initial") - restore := testbase.PatchEnvironment(envName, "new value") - // Using check to make sure the environment gets set back properly in the test. - c.Check(os.Getenv(envName), gc.Equals, "new value") - restore() - c.Check(os.Getenv(envName), gc.Equals, "initial") - os.Setenv(envName, oldValue) -} - -func (*PatchEnvironmentSuite) TestRestorerAdd(c *gc.C) { - var order []string - first := testbase.Restorer(func() { order = append(order, "first") }) - second := testbase.Restorer(func() { order = append(order, "second") }) - restore := first.Add(second) - restore() - c.Assert(order, gc.DeepEquals, []string{"second", "first"}) -} - -func (*PatchEnvironmentSuite) TestPatchEnvPathPrepend(c *gc.C) { - oldPath := os.Getenv("PATH") - dir := "/bin/bar" - - // just in case something goes wrong - defer os.Setenv("PATH", oldPath) - - restore := testbase.PatchEnvPathPrepend(dir) - - expect := dir + string(os.PathListSeparator) + oldPath - c.Check(os.Getenv("PATH"), gc.Equals, expect) - restore() - c.Check(os.Getenv("PATH"), gc.Equals, oldPath) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/tools/tools.go juju-core-1.17.6/src/launchpad.net/juju-core/tools/tools.go --- juju-core-1.17.4/src/launchpad.net/juju-core/tools/tools.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/tools/tools.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,7 @@ package tools import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/version" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/agentconfig.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/agentconfig.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/agentconfig.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/agentconfig.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,150 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strconv" + + "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/state/api/params" +) + +var ( + rootLogDir = "/var/log" + rootSpoolDir = "/var/spool/rsyslog" +) + +var chownPath = func(path, username string) error { + user, err := user.Lookup(username) + if err != nil { + return fmt.Errorf("cannot lookup %q user id: %v", username, err) + } + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return fmt.Errorf("invalid user id %q: %v", user.Uid, err) + } + gid, err := strconv.Atoi(user.Gid) + if err != nil { + return fmt.Errorf("invalid group id %q: %v", user.Gid, err) + } + return os.Chown(path, uid, gid) +} + +var isLocalEnviron = func(envConfig *config.Config) bool { + return envConfig.Type() == "local" +} + +func migrateLocalProviderAgentConfig(context Context) error { + st := context.State() + if st == nil { + logger.Debugf("no state connection, no migration required") + // We're running on a different node than the state server. + return nil + } + envConfig, err := st.EnvironConfig() + if err != nil { + return fmt.Errorf("failed to read current config: %v", err) + } + if !isLocalEnviron(envConfig) { + logger.Debugf("not a local environment, no migration required") + return nil + } + attrs := envConfig.AllAttrs() + rootDir, _ := attrs["root-dir"].(string) + sharedStorageDir := filepath.Join(rootDir, "shared-storage") + // In case these two are empty we need to set them and update the + // environment config. + namespace, _ := attrs["namespace"].(string) + container, _ := attrs["container"].(string) + + if namespace == "" { + username := os.Getenv("USER") + if username == "root" { + // sudo was probably called, get the original user. + username = os.Getenv("SUDO_USER") + } + if username == "" { + return fmt.Errorf("cannot get current user from the environment: %v", os.Environ()) + } + namespace = username + "-" + envConfig.Name() + } + if container == "" { + container = "lxc" + } + + dataDir := rootDir + localLogDir := filepath.Join(rootDir, "log") + // rsyslogd is restricted to write to /var/log + logDir := fmt.Sprintf("%s/juju-%s", rootLogDir, namespace) + jobs := []params.MachineJob{params.JobManageEnviron} + values := map[string]string{ + agent.Namespace: namespace, + // ContainerType is empty on the bootstrap node. + agent.ContainerType: "", + agent.AgentServiceName: "juju-agent-" + namespace, + agent.MongoServiceName: "juju-db-" + namespace, + } + deprecatedValues := []string{ + "SHARED_STORAGE_ADDR", + "SHARED_STORAGE_DIR", + } + + // Remove shared-storage dir if there. + if err := os.RemoveAll(sharedStorageDir); err != nil { + return fmt.Errorf("cannot remove deprecated %q: %v", sharedStorageDir, err) + } + + // We need to create the dirs if they don't exist. + if err := os.MkdirAll(dataDir, 0755); err != nil { + return fmt.Errorf("cannot create dataDir %q: %v", dataDir, err) + } + // We always recreate the logDir to make sure it's empty. + if err := os.RemoveAll(logDir); err != nil { + return fmt.Errorf("cannot remove logDir %q: %v", logDir, err) + } + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("cannot create logDir %q: %v", logDir, err) + } + // Reconfigure rsyslog as needed: + // 1. logDir must be owned by syslog:adm + // 2. Remove old rsyslog spool config + // 3. Relink logs to the new logDir + if err := chownPath(logDir, "syslog"); err != nil { + return err + } + spoolConfig := fmt.Sprintf("%s/machine-0-%s", rootSpoolDir, namespace) + if err := os.RemoveAll(spoolConfig); err != nil { + return fmt.Errorf("cannot remove %q: %v", spoolConfig, err) + } + allMachinesLog := filepath.Join(logDir, "all-machines.log") + if err := os.Symlink(allMachinesLog, localLogDir+"/"); err != nil && !os.IsExist(err) { + return fmt.Errorf("cannot symlink %q to %q: %v", allMachinesLog, localLogDir, err) + } + machine0Log := filepath.Join(localLogDir, "machine-0.log") + if err := os.Symlink(machine0Log, logDir+"/"); err != nil && !os.IsExist(err) { + return fmt.Errorf("cannot symlink %q to %q: %v", machine0Log, logDir, err) + } + + newCfg := map[string]interface{}{ + "namespace": namespace, + "container": container, + } + if err := st.UpdateEnvironConfig(newCfg, nil, nil); err != nil { + return fmt.Errorf("cannot update environment config: %v", err) + } + + migrateParams := agent.MigrateConfigParams{ + DataDir: dataDir, + LogDir: logDir, + Jobs: jobs, + Values: values, + DeleteValues: deprecatedValues, + } + return agent.MigrateConfig(context.AgentConfig(), migrateParams) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/agentconfig_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/agentconfig_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/agentconfig_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/agentconfig_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,178 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades_test + +import ( + "os" + "path/filepath" + + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/environs/config" + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/upgrades" + "launchpad.net/juju-core/version" +) + +type migrateLocalProviderAgentConfigSuite struct { + jujutesting.JujuConnSuite + ctx upgrades.Context +} + +var _ = gc.Suite(&migrateLocalProviderAgentConfigSuite{}) + +func (s *migrateLocalProviderAgentConfigSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + // Make sure we fallback to SUDO_USER if USER is root. + s.PatchEnvironment("USER", "root") + s.PatchEnvironment("SUDO_USER", "user") + s.PatchValue(upgrades.RootLogDir, c.MkDir()) + s.PatchValue(upgrades.RootSpoolDir, c.MkDir()) + s.PatchValue(&agent.DefaultDataDir, c.MkDir()) + s.PatchValue(upgrades.ChownPath, func(_, _ string) error { return nil }) + s.PatchValue(upgrades.IsLocalEnviron, func(_ *config.Config) bool { return true }) +} + +func (s *migrateLocalProviderAgentConfigSuite) primeConfig(c *gc.C, st *state.State, job state.MachineJob, tag string) { + rootDir := c.MkDir() + sharedStorageDir := filepath.Join(rootDir, "shared-storage") + c.Assert(os.MkdirAll(sharedStorageDir, 0755), gc.IsNil) + localLogDir := filepath.Join(rootDir, "log") + c.Assert(os.MkdirAll(localLogDir, 0755), gc.IsNil) + + initialConfig, err := agent.NewAgentConfig(agent.AgentConfigParams{ + Tag: tag, + Password: "blah", + CACert: []byte(testing.CACert), + StateAddresses: []string{"localhost:1111"}, + DataDir: agent.DefaultDataDir, + LogDir: agent.DefaultLogDir, + UpgradedToVersion: version.MustParse("1.16.0"), + Values: map[string]string{ + "SHARED_STORAGE_ADDR": "blah", + "SHARED_STORAGE_DIR": sharedStorageDir, + }, + }) + c.Assert(err, gc.IsNil) + c.Assert(initialConfig.Write(), gc.IsNil) + + apiState, _ := s.OpenAPIAsNewMachine(c, job) + s.ctx = &mockContext{ + realAgentConfig: initialConfig, + apiState: apiState, + state: st, + } + + newCfg := (map[string]interface{}{ + "root-dir": rootDir, + }) + err = s.State.UpdateEnvironConfig(newCfg, nil, nil) + c.Assert(err, gc.IsNil) +} + +func (s *migrateLocalProviderAgentConfigSuite) assertConfigProcessed(c *gc.C) { + envConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + allAttrs := envConfig.AllAttrs() + + namespace, _ := allAttrs["namespace"].(string) + c.Assert(namespace, gc.Equals, "user-dummyenv") + container, _ := allAttrs["container"].(string) + c.Assert(container, gc.Equals, "lxc") + + expectedDataDir, _ := allAttrs["root-dir"].(string) + expectedSharedStorageDir := filepath.Join(expectedDataDir, "shared-storage") + _, err = os.Lstat(expectedSharedStorageDir) + c.Assert(err, gc.NotNil) + c.Assert(err, jc.Satisfies, os.IsNotExist) + expectedLogDir := filepath.Join(*upgrades.RootLogDir, "juju-"+namespace) + expectedJobs := []params.MachineJob{params.JobManageEnviron} + tag := s.ctx.AgentConfig().Tag() + + // We need to read the actual migrated agent config. + configFilePath := agent.ConfigPath(expectedDataDir, tag) + agentConfig, err := agent.ReadConf(configFilePath) + c.Assert(err, gc.IsNil) + + c.Assert(agentConfig.DataDir(), gc.Equals, expectedDataDir) + c.Assert(agentConfig.LogDir(), gc.Equals, expectedLogDir) + c.Assert(agentConfig.Jobs(), gc.DeepEquals, expectedJobs) + c.Assert(agentConfig.Value("SHARED_STORAGE_ADDR"), gc.Equals, "") + c.Assert(agentConfig.Value("SHARED_STORAGE_DIR"), gc.Equals, "") + c.Assert(agentConfig.Value(agent.Namespace), gc.Equals, namespace) + agentService := "juju-agent-user-dummyenv" + mongoService := "juju-db-user-dummyenv" + c.Assert(agentConfig.Value(agent.AgentServiceName), gc.Equals, agentService) + c.Assert(agentConfig.Value(agent.MongoServiceName), gc.Equals, mongoService) + c.Assert(agentConfig.Value(agent.ContainerType), gc.Equals, "") +} + +func (s *migrateLocalProviderAgentConfigSuite) assertConfigNotProcessed(c *gc.C) { + envConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + allAttrs := envConfig.AllAttrs() + + namespace, _ := allAttrs["namespace"].(string) + c.Assert(namespace, gc.Equals, "") + container, _ := allAttrs["container"].(string) + c.Assert(container, gc.Equals, "") + + rootDir, _ := allAttrs["root-dir"].(string) + expectedSharedStorageDir := filepath.Join(rootDir, "shared-storage") + _, err = os.Lstat(expectedSharedStorageDir) + c.Assert(err, gc.IsNil) + tag := s.ctx.AgentConfig().Tag() + + // We need to read the actual migrated agent config. + configFilePath := agent.ConfigPath(agent.DefaultDataDir, tag) + agentConfig, err := agent.ReadConf(configFilePath) + c.Assert(err, gc.IsNil) + + c.Assert(agentConfig.DataDir(), gc.Equals, agent.DefaultDataDir) + c.Assert(agentConfig.LogDir(), gc.Equals, agent.DefaultLogDir) + c.Assert(agentConfig.Jobs(), gc.HasLen, 0) + c.Assert(agentConfig.Value("SHARED_STORAGE_ADDR"), gc.Equals, "blah") + c.Assert(agentConfig.Value("SHARED_STORAGE_DIR"), gc.Equals, expectedSharedStorageDir) + c.Assert(agentConfig.Value(agent.Namespace), gc.Equals, "") + c.Assert(agentConfig.Value(agent.AgentServiceName), gc.Equals, "") + c.Assert(agentConfig.Value(agent.MongoServiceName), gc.Equals, "") + c.Assert(agentConfig.Value(agent.ContainerType), gc.Equals, "") +} +func (s *migrateLocalProviderAgentConfigSuite) TestMigrateStateServer(c *gc.C) { + s.primeConfig(c, s.State, state.JobManageEnviron, "machine-0") + err := upgrades.MigrateLocalProviderAgentConfig(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) +} + +func (s *migrateLocalProviderAgentConfigSuite) TestMigrateNonLocalEnvNotDone(c *gc.C) { + s.PatchValue(upgrades.IsLocalEnviron, func(_ *config.Config) bool { return false }) + s.primeConfig(c, s.State, state.JobManageEnviron, "machine-0") + err := upgrades.MigrateLocalProviderAgentConfig(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigNotProcessed(c) +} + +func (s *migrateLocalProviderAgentConfigSuite) TestMigrateWithoutStateConnectionNotDone(c *gc.C) { + s.primeConfig(c, nil, state.JobManageEnviron, "machine-0") + err := upgrades.MigrateLocalProviderAgentConfig(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigNotProcessed(c) +} + +func (s *migrateLocalProviderAgentConfigSuite) TestIdempotent(c *gc.C) { + s.primeConfig(c, s.State, state.JobManageEnviron, "machine-0") + err := upgrades.MigrateLocalProviderAgentConfig(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) + + err = upgrades.MigrateLocalProviderAgentConfig(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,18 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades + +func processDeprecatedEnvSettings(context Context) error { + st := context.State() + removeAttrs := []string{ + "public-bucket", + "public-bucket-region", + "public-bucket-url", + "default-image-id", + "default-instance-type", + "shared-storage-port", + } + // TODO (wallyworld) - delete tools-url in 1.20 + return st.UpdateEnvironConfig(map[string]interface{}{}, removeAttrs, nil) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/deprecatedenvsettings_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,82 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades_test + +import ( + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/upgrades" +) + +type processDeprecatedEnvSettingsSuite struct { + jujutesting.JujuConnSuite + ctx upgrades.Context +} + +var _ = gc.Suite(&processDeprecatedEnvSettingsSuite{}) + +func (s *processDeprecatedEnvSettingsSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + apiState, _ := s.OpenAPIAsNewMachine(c, state.JobManageEnviron) + s.ctx = &mockContext{ + agentConfig: &mockAgentConfig{dataDir: s.DataDir()}, + apiState: apiState, + state: s.State, + } + // Add in old environment settings. + newCfg := map[string]interface{}{ + "public-bucket": "foo", + "public-bucket-region": "bar", + "public-bucket-url": "shazbot", + "default-instance-type": "vulch", + "default-image-id": "1234", + "shared-storage-port": 1234, + } + err := s.State.UpdateEnvironConfig(newCfg, nil, nil) + c.Assert(err, gc.IsNil) +} + +func (s *processDeprecatedEnvSettingsSuite) TestEnvSettingsSet(c *gc.C) { + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + allAttrs := cfg.AllAttrs() + c.Assert(allAttrs["public-bucket"], gc.Equals, "foo") + c.Assert(allAttrs["public-bucket-region"], gc.Equals, "bar") + c.Assert(allAttrs["public-bucket-url"], gc.Equals, "shazbot") + c.Assert(allAttrs["default-instance-type"], gc.Equals, "vulch") + c.Assert(allAttrs["default-image-id"], gc.Equals, "1234") + c.Assert(allAttrs["shared-storage-port"], gc.Equals, 1234) +} + +func (s *processDeprecatedEnvSettingsSuite) assertConfigProcessed(c *gc.C) { + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + allAttrs := cfg.AllAttrs() + for _, deprecated := range []string{ + "public-bucket", "public-bucket-region", "public-bucket-url", + "default-image-id", "default-instance-type", "shared-storage-port", + } { + _, ok := allAttrs[deprecated] + c.Assert(ok, jc.IsFalse) + } +} + +func (s *processDeprecatedEnvSettingsSuite) TestOldConfigRemoved(c *gc.C) { + err := upgrades.ProcessDeprecatedEnvSettings(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) +} + +func (s *processDeprecatedEnvSettingsSuite) TestIdempotent(c *gc.C) { + err := upgrades.ProcessDeprecatedEnvSettings(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) + + err = upgrades.ProcessDeprecatedEnvSettings(s.ctx) + c.Assert(err, gc.IsNil) + s.assertConfigProcessed(c) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,9 +6,17 @@ var ( UpgradeOperations = &upgradeOperations UbuntuHome = &ubuntuHome + RootLogDir = &rootLogDir + RootSpoolDir = &rootSpoolDir + + ChownPath = &chownPath + IsLocalEnviron = &isLocalEnviron // 118 upgrade functions StepsFor118 = stepsFor118 EnsureLockDirExistsAndUbuntuWritable = ensureLockDirExistsAndUbuntuWritable EnsureSystemSSHKey = ensureSystemSSHKey + UpdateRsyslogPort = updateRsyslogPort + ProcessDeprecatedEnvSettings = processDeprecatedEnvSettings + MigrateLocalProviderAgentConfig = migrateLocalProviderAgentConfig ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/file.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/file.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/file.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/file.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package upgrades - -import ( - "io/ioutil" - "os" - "path/filepath" -) - -// WriteReplacementFile create a file called filename with the specified -// contents, allowing for the fact that filename may already exist. -// It first writes data to a temp file and then renames so that if the -// writing fails, any original content is preserved. -func WriteReplacementFile(filename string, data []byte, perm os.FileMode) error { - // Write the data to a temp file - confDir := filepath.Dir(filename) - tempDir, err := ioutil.TempDir(confDir, "") - if err != nil { - return err - } - tempFile := filepath.Join(tempDir, "newfile") - defer os.RemoveAll(tempDir) - err = ioutil.WriteFile(tempFile, []byte(data), perm) - if err != nil { - return err - } - return os.Rename(tempFile, filename) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/file_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/file_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/file_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/file_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package upgrades_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" - "launchpad.net/juju-core/upgrades" -) - -type fileSuite struct { - testbase.LoggingSuite -} - -var _ = gc.Suite(&fileSuite{}) - -func checkFile(c *gc.C, filename string) { - context, err := ioutil.ReadFile(filename) - c.Assert(string(context), gc.Equals, "world") - fi, err := os.Stat(filename) - c.Assert(err, gc.IsNil) - mode := os.FileMode(0644) - c.Assert(fi.Mode()&(os.ModeType|mode), gc.Equals, mode) -} - -func (s *fileSuite) TestWriteReplacementFileWhenNotExists(c *gc.C) { - dir := c.MkDir() - filename := filepath.Join(dir, "foo.conf") - err := upgrades.WriteReplacementFile(filename, []byte("world"), 0644) - c.Assert(err, gc.IsNil) - checkFile(c, filename) -} - -func (s *fileSuite) TestWriteReplacementFileWhenExists(c *gc.C) { - dir := c.MkDir() - filename := filepath.Join(dir, "foo.conf") - err := ioutil.WriteFile(filename, []byte("hello"), 0755) - c.Assert(err, gc.IsNil) - err = upgrades.WriteReplacementFile(filename, []byte("world"), 0644) - c.Assert(err, gc.IsNil) - checkFile(c, filename) -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/lockdirectory_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/lockdirectory_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/lockdirectory_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/lockdirectory_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,11 +8,11 @@ "io/ioutil" "path/filepath" - "github.com/loggo/loggo" + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/upgrades" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/rsyslogport.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/rsyslogport.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/rsyslogport.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/rsyslogport.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,16 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades + +import ( + "launchpad.net/juju-core/environs/config" +) + +func updateRsyslogPort(context Context) error { + st := context.State() + attrs := map[string]interface{}{ + "syslog-port": config.DefaultSyslogPort, + } + return st.UpdateEnvironConfig(attrs, nil, nil) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/rsyslogport_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/rsyslogport_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/rsyslogport_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/rsyslogport_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,51 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades_test + +import ( + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs/config" + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/upgrades" +) + +type rsyslogPortSuite struct { + jujutesting.JujuConnSuite + ctx upgrades.Context +} + +var _ = gc.Suite(&rsyslogPortSuite{}) + +func (s *rsyslogPortSuite) SetUpTest(c *gc.C) { + s.JujuConnSuite.SetUpTest(c) + apiState, _ := s.OpenAPIAsNewMachine(c, state.JobManageEnviron) + s.ctx = &mockContext{ + agentConfig: &mockAgentConfig{dataDir: s.DataDir()}, + apiState: apiState, + state: s.State, + } + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + c.Assert(cfg.SyslogPort(), gc.Not(gc.Equals), config.DefaultSyslogPort) +} + +func (s *rsyslogPortSuite) TestSyslogPortChanged(c *gc.C) { + err := upgrades.UpdateRsyslogPort(s.ctx) + c.Assert(err, gc.IsNil) + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + c.Assert(cfg.SyslogPort(), gc.Equals, config.DefaultSyslogPort) +} + +func (s *rsyslogPortSuite) TestIdempotent(c *gc.C) { + err := upgrades.UpdateRsyslogPort(s.ctx) + c.Assert(err, gc.IsNil) + err = upgrades.UpdateRsyslogPort(s.ctx) + c.Assert(err, gc.IsNil) + cfg, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + c.Assert(cfg.SyslogPort(), gc.Equals, config.DefaultSyslogPort) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/steps118.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/steps118.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118.go 2014-03-20 12:52:38.000000000 +0000 @@ -17,9 +17,24 @@ run: ensureSystemSSHKey, }, &upgradeStep{ + description: "update rsyslog port", + targets: []Target{StateServer}, + run: updateRsyslogPort, + }, + &upgradeStep{ description: "install rsyslog-gnutls", targets: []Target{AllMachines}, run: installRsyslogGnutls, }, + &upgradeStep{ + description: "remove deprecated environment config settings", + targets: []Target{StateServer}, + run: processDeprecatedEnvSettings, + }, + &upgradeStep{ + description: "migrate local provider agent config", + targets: []Target{StateServer}, + run: migrateLocalProviderAgentConfig, + }, } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/steps118_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/steps118_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -19,7 +19,10 @@ var expectedSteps = []string{ "make $DATADIR/locks owned by ubuntu:ubuntu", "generate system ssh key", + "update rsyslog port", "install rsyslog-gnutls", + "remove deprecated environment config settings", + "migrate local provider agent config", } func (s *steps118Suite) TestUpgradeOperationsContent(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/systemsshkey_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/systemsshkey_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/systemsshkey_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/systemsshkey_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,11 +8,11 @@ "os" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/upgrades" "launchpad.net/juju-core/utils/ssh" ) @@ -34,7 +34,7 @@ _, err := os.Stat(s.keyFile()) c.Assert(err, jc.Satisfies, os.IsNotExist) // There's initially one authorised key for the test user. - cfg, err := s.JujuConnSuite.State.EnvironConfig() + cfg, err := s.State.EnvironConfig() c.Assert(err, gc.IsNil) authKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) c.Assert(authKeys, gc.HasLen, 1) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/upgrade.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/upgrade.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/upgrade.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/upgrade.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,9 +6,10 @@ import ( "fmt" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/version" ) @@ -78,7 +79,12 @@ // APIState returns an API connection to state. APIState() *api.State - // AgentConfig returns the agent config for the machine that is being upgraded. + // State returns a connection to state. This will be non-nil + // only in the context of a state server. + State() *state.State + + // AgentConfig returns the agent config for the machine that is being + // upgraded. AgentConfig() agent.Config } @@ -87,12 +93,18 @@ // Work in progress........ // Exactly what a context needs is to be determined as the // implementation evolves. - st *api.State + api *api.State + st *state.State agentConfig agent.Config } // APIState is defined on the Context interface. func (c *upgradeContext) APIState() *api.State { + return c.api +} + +// State is defined on the Context interface. +func (c *upgradeContext) State() *state.State { return c.st } @@ -102,8 +114,9 @@ } // NewContext returns a new upgrade context. -func NewContext(agentConfig agent.Config, st *api.State) Context { +func NewContext(agentConfig agent.Config, api *api.State, st *state.State) Context { return &upgradeContext{ + api: api, st: st, agentConfig: agentConfig, } @@ -146,7 +159,7 @@ // validTarget returns true if target is in step.Targets(). func validTarget(target Target, step Step) bool { for _, opTarget := range step.Targets() { - if target == AllMachines || target == opTarget { + if opTarget == AllMachines || target == opTarget { return true } } @@ -163,7 +176,7 @@ if !validTarget(target, step) { continue } - logger.Infof("Running upgrade step: %v", step.Description()) + logger.Infof("running upgrade step on target %q: %v", target, step.Description()) if err := step.Run(context); err != nil { logger.Errorf("upgrade step %q failed: %v", step.Description(), err) return &upgradeError{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/upgrade_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/upgrade_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upgrades/upgrade_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/upgrade_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,12 +8,14 @@ "strings" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/upgrades" "launchpad.net/juju-core/version" @@ -75,25 +77,36 @@ } type mockContext struct { - messages []string - agentConfig *mockAgentConfig - apiState *api.State + messages []string + agentConfig *mockAgentConfig + realAgentConfig agent.Config + apiState *api.State + state *state.State } func (c *mockContext) APIState() *api.State { return c.apiState } +func (c *mockContext) State() *state.State { + return c.state +} + func (c *mockContext) AgentConfig() agent.Config { + if c.realAgentConfig != nil { + return c.realAgentConfig + } return c.agentConfig } type mockAgentConfig struct { agent.Config dataDir string + logDir string tag string - namespace string + jobs []params.MachineJob apiAddresses []string + values map[string]string } func (mock *mockAgentConfig) Tag() string { @@ -104,15 +117,20 @@ return mock.dataDir } +func (mock *mockAgentConfig) LogDir() string { + return mock.logDir +} + +func (mock *mockAgentConfig) Jobs() []params.MachineJob { + return mock.jobs +} + func (mock *mockAgentConfig) APIAddresses() ([]string, error) { return mock.apiAddresses, nil } func (mock *mockAgentConfig) Value(name string) string { - if name == agent.Namespace { - return mock.namespace - } - return "" + return mock.values[name] } func targets(targets ...upgrades.Target) (upgradeTargets []upgrades.Target) { @@ -160,6 +178,14 @@ &mockUpgradeStep{"step 2 - 1.18.0", targets(upgrades.StateServer)}, }, }, + &mockUpgradeOperation{ + targetVersion: version.MustParse("1.20.0"), + steps: []upgrades.Step{ + &mockUpgradeStep{"step 1 - 1.20.0", targets(upgrades.AllMachines)}, + &mockUpgradeStep{"step 2 - 1.20.0", targets(upgrades.HostMachine)}, + &mockUpgradeStep{"step 3 - 1.20.0", targets(upgrades.StateServer)}, + }, + }, } return steps } @@ -200,9 +226,17 @@ }, { about: "allMachines matches everything", - fromVersion: "1.17.1", - target: upgrades.AllMachines, - expectedSteps: []string{"step 1 - 1.18.0", "step 2 - 1.18.0"}, + fromVersion: "1.18.1", + toVersion: "1.20.0", + target: upgrades.HostMachine, + expectedSteps: []string{"step 1 - 1.20.0", "step 2 - 1.20.0"}, + }, + { + about: "allMachines matches everything", + fromVersion: "1.18.1", + toVersion: "1.20.0", + target: upgrades.StateServer, + expectedSteps: []string{"step 1 - 1.20.0", "step 3 - 1.20.0"}, }, { about: "error aborts, subsequent steps not run", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upstart/service.go juju-core-1.17.6/src/launchpad.net/juju-core/upstart/service.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upstart/service.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upstart/service.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,58 +5,15 @@ import ( "fmt" - "os" "path" "launchpad.net/juju-core/utils" ) const ( - maxMongoFiles = 65000 maxAgentFiles = 20000 ) -// JujuMongodPath is the path of the mongod that is bundled specifically for juju. -// This value is public only for testing purposes, please do not change. -var JujuMongodPath = "/usr/lib/juju/bin/mongod" - -// MongoPath returns the executable path to be used to run mongod on this machine. -// If the juju-bundled version of mongo exists, it will return that path, otherwise -// it will return the command to run mongod from the path. -func MongodPath() string { - if _, err := os.Stat(JujuMongodPath); err == nil { - return JujuMongodPath - } - - // just use whatever is in the path - return "mongod" -} - -// MongoUpstartService returns the upstart config for the mongo state service. -func MongoUpstartService(name, dataDir, dbDir string, port int) *Conf { - keyFile := path.Join(dataDir, "server.pem") - svc := NewService(name) - return &Conf{ - Service: *svc, - Desc: "juju state database", - Limit: map[string]string{ - "nofile": fmt.Sprintf("%d %d", maxMongoFiles, maxMongoFiles), - "nproc": fmt.Sprintf("%d %d", maxAgentFiles, maxAgentFiles), - }, - Cmd: MongodPath() + - " --auth" + - " --dbpath=" + dbDir + - " --sslOnNormalPorts" + - " --sslPEMKeyFile " + utils.ShQuote(keyFile) + - " --sslPEMKeyPassword ignored" + - " --bind_ip 0.0.0.0" + - " --port " + fmt.Sprint(port) + - " --noprealloc" + - " --syslog" + - " --smallfiles", - } -} - // MachineAgentUpstartService returns the upstart config for a machine agent // based on the tag and machineId passed in. func MachineAgentUpstartService(name, toolsDir, dataDir, logDir, tag, machineId string, env map[string]string) *Conf { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upstart/upstart.go juju-core-1.17.6/src/launchpad.net/juju-core/upstart/upstart.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upstart/upstart.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upstart/upstart.go 2014-03-20 12:52:38.000000000 +0000 @@ -20,6 +20,9 @@ var startedRE = regexp.MustCompile(`^.* start/running, process (\d+)\n$`) +// InitDir holds the default init directory name. +var InitDir = "/etc/init" + var InstallStartRetryAttempts = utils.AttemptStrategy{ Total: 1 * time.Second, Delay: 250 * time.Millisecond, @@ -32,7 +35,7 @@ } func NewService(name string) *Service { - return &Service{Name: name, InitDir: "/etc/init"} + return &Service{Name: name, InitDir: InitDir} } // confPath returns the path to the service's configuration file. diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/upstart/upstart_test.go juju-core-1.17.6/src/launchpad.net/juju-core/upstart/upstart_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/upstart/upstart_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/upstart/upstart_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,9 +10,9 @@ "path/filepath" "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/upstart" "launchpad.net/juju-core/utils" @@ -254,23 +254,3 @@ c.Assert(err, gc.IsNil) c.Assert(&conf.Service, jc.Satisfies, (*upstart.Service).Running) } - -func (s *UpstartSuite) TestJujuMongodPath(c *gc.C) { - d := c.MkDir() - defer os.RemoveAll(d) - mongoPath := filepath.Join(d, "mongod") - upstart.JujuMongodPath = mongoPath - - err := ioutil.WriteFile(mongoPath, []byte{}, 0777) - c.Assert(err, gc.IsNil) - - obtained := upstart.MongodPath() - c.Assert(obtained, gc.Equals, mongoPath) -} - -func (s *UpstartSuite) TestDefaultMongodPath(c *gc.C) { - upstart.JujuMongodPath = "/not/going/to/exist/mongod" - - obtained := upstart.MongodPath() - c.Assert(obtained, gc.Equals, "mongod") -} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/apt.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/apt.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,7 +11,7 @@ "regexp" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/juju/osenv" ) @@ -42,6 +42,92 @@ // the user var aptGetEnvOptions = []string{"DEBIAN_FRONTEND=noninteractive"} +// cloudArchivePackages maintaines a list of packages that AptGetPreparePackages +// should reference when determining the --target-release for a given series. +// http://reqorts.qa.ubuntu.com/reports/ubuntu-server/cloud-archive/cloud-tools_versions.html +var cloudArchivePackages = map[string]bool{ + "cloud-utils": true, + "curtin": true, + "djorm-ext-pgarray": true, + "golang": true, + "iproute2": true, + "isc-dhcp": true, + "juju-core": true, + "libseccomp": true, + "libv8-3.14": true, + "lxc": true, + "maas": true, + "mongodb": true, + "python-django": true, + "python-django-piston": true, + "python-jujuclient": true, + "python-tx-tftp": true, + "python-websocket-client": true, + "raphael 2.1.0-1ubuntu1": true, + "simplestreams": true, + "txlongpoll": true, + "uvtool": true, + "yui3": true, +} + +// targetRelease returns a string base on the current series +// that is suitable for use with the apt-get --target-release option +func targetRelease(series string) string { + switch series { + case "precise": + return "precise-updates/cloud-tools" + default: + return "" + } +} + +// AptGetCommand returns a command to execute apt-get +// with the specified arguments, and the appropriate +// environment variables and options for a non-interactive +// session. +func AptGetCommand(args ...string) []string { + cmd := append([]string{"env"}, aptGetEnvOptions...) + cmd = append(cmd, aptGetCommand...) + return append(cmd, args...) +} + +// AptGetPreparePackages returns a slice of installCommands. Each item +// in the slice is suitable for passing directly to AptGetInstall. +// +// AptGetPreparePackages will inspect the series passed to it +// and properly generate an installCommand entry with a --target-release +// should the series be an LTS release with cloud archive packages. +func AptGetPreparePackages(packages []string, series string) [][]string { + var installCommands [][]string + if target := targetRelease(series); target == "" { + return append(installCommands, packages) + } else { + var pkgs []string + pkgs_with_target := []string{"--target-release", target} + for _, pkg := range packages { + if cloudArchivePackages[pkg] { + pkgs_with_target = append(pkgs_with_target, pkg) + } else { + pkgs = append(pkgs, pkg) + } + } + + // We check for >2 here so that we only append pkgs_with_target + // if there was an actual package in the slice. + if len(pkgs_with_target) > 2 { + installCommands = append(installCommands, pkgs_with_target) + } + + // Sometimes we may end up with all cloudArchivePackages + // in that case we do not want to append an empty slice of pkgs + if len(pkgs) > 0 { + installCommands = append(installCommands, pkgs) + } + + return installCommands + } +} + // AptGetInstall runs 'apt-get install packages' for the packages listed here func AptGetInstall(packages ...string) error { cmdArgs := append([]string(nil), aptGetCommand...) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/apt_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/apt_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,10 +6,10 @@ import ( "fmt" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/osenv" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" ) @@ -33,6 +33,28 @@ c.Assert(cmd.Env[len(cmd.Env)-1], gc.Equals, "DEBIAN_FRONTEND=noninteractive") } +func (s *AptSuite) TestAptGetPreparePackages(c *gc.C) { + packagesList := utils.AptGetPreparePackages([]string{"lxc", "bridge-utils", "git", "mongodb"}, "precise") + c.Assert(packagesList[0], gc.DeepEquals, []string{"--target-release", "precise-updates/cloud-tools", "lxc", "mongodb"}) + c.Assert(packagesList[1], gc.DeepEquals, []string{"bridge-utils", "git"}) +} + +func (s *AptSuite) TestAptGetCommand(c *gc.C) { + s.testAptGetCommand(c) + s.testAptGetCommand(c, "install", "foo") +} + +func (s *AptSuite) testAptGetCommand(c *gc.C, args ...string) { + commonArgs := []string{ + "env", "DEBIAN_FRONTEND=noninteractive", + "apt-get", "--option=Dpkg::Options::=--force-confold", + "--option=Dpkg::options::=--force-unsafe-io", "--assume-yes", "--quiet", + } + expected := append(commonArgs, args...) + cmd := utils.AptGetCommand(args...) + c.Assert(cmd, gc.DeepEquals, expected) +} + func (s *AptSuite) TestAptGetError(c *gc.C) { const expected = `E: frobnicator failure detected` cmdError := fmt.Errorf("error") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/exec/exec.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/exec/exec.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/exec/exec.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/exec/exec.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "os/exec" "syscall" - "github.com/loggo/loggo" + "github.com/juju/loggo" ) var logger = loggo.GetLogger("juju.util.exec") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/exec/exec_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/exec/exec_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/exec/exec_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/exec/exec_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,9 +4,9 @@ package exec_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/exec" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,9 @@ +// Copyright 2013 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package utils + +var ( + GOMAXPROCS = &gomaxprocs + NumCPU = &numCPU +) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/file.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/file.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/file.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/file.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" "io" + "io/ioutil" "os" "os/user" "path" @@ -98,3 +99,43 @@ _, err = io.Copy(df, f) return err } + +// AtomicWriteFileAndChange atomically writes the filename with the +// given contents and calls the given function after the contents were +// written, but before the file is renamed. +func AtomicWriteFileAndChange(filename string, contents []byte, change func(*os.File) error) (err error) { + dir, file := filepath.Split(filename) + f, err := ioutil.TempFile(dir, file) + if err != nil { + return fmt.Errorf("cannot create temp file: %v", err) + } + defer f.Close() + defer func() { + if err != nil { + // Don't leave the temp file lying around on error. + os.Remove(f.Name()) + } + }() + if _, err := f.Write(contents); err != nil { + return fmt.Errorf("cannot write %q contents: %v", filename, err) + } + if err := change(f); err != nil { + return err + } + if err := ReplaceFile(f.Name(), filename); err != nil { + return fmt.Errorf("cannot replace %q with %q: %v", f.Name(), filename, err) + } + return nil +} + +// AtomicWriteFile atomically writes the filename with the given +// contents and permissions, replacing any existing file at the same +// path. +func AtomicWriteFile(filename string, contents []byte, perms os.FileMode) (err error) { + return AtomicWriteFileAndChange(filename, contents, func(f *os.File) error { + if err := f.Chmod(perms); err != nil { + return fmt.Errorf("cannot set permissions: %v", err) + } + return nil + }) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/file_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/file_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/file_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/file_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,10 +4,13 @@ package utils_test import ( + "fmt" "io/ioutil" + "os" "os/user" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/juju/osenv" @@ -88,3 +91,97 @@ c.Assert(err, gc.IsNil) c.Assert(string(data), gc.Equals, "hello world") } + +var atomicWriteFileTests = []struct { + summary string + change func(filename string, contents []byte) error + check func(c *gc.C, fileInfo os.FileInfo) + expectErr string +}{{ + summary: "atomic file write and chmod 0644", + change: func(filename string, contents []byte) error { + return utils.AtomicWriteFile(filename, contents, 0765) + }, + check: func(c *gc.C, fi os.FileInfo) { + c.Assert(fi.Mode(), gc.Equals, 0765) + }, +}, { + summary: "atomic file write and change", + change: func(filename string, contents []byte) error { + chmodChange := func(f *os.File) error { + return f.Chmod(0700) + } + return utils.AtomicWriteFileAndChange(filename, contents, chmodChange) + }, + check: func(c *gc.C, fi os.FileInfo) { + c.Assert(fi.Mode(), gc.Equals, 0700) + }, +}, { + summary: "atomic file write empty contents", + change: func(filename string, contents []byte) error { + nopChange := func(*os.File) error { + return nil + } + return utils.AtomicWriteFileAndChange(filename, contents, nopChange) + }, +}, { + summary: "atomic file write and failing change func", + change: func(filename string, contents []byte) error { + errChange := func(*os.File) error { + return fmt.Errorf("pow!") + } + return utils.AtomicWriteFileAndChange(filename, contents, errChange) + }, + expectErr: "pow!", +}} + +func (*fileSuite) TestAtomicWriteFile(c *gc.C) { + dir := c.MkDir() + name := "test.file" + path := filepath.Join(dir, name) + assertDirContents := func(names ...string) { + fis, err := ioutil.ReadDir(dir) + c.Assert(err, gc.IsNil) + c.Assert(fis, gc.HasLen, len(names)) + for i, name := range names { + c.Assert(fis[i].Name(), gc.Equals, name) + } + } + assertNotExist := func(path string) { + _, err := os.Lstat(path) + c.Assert(err, jc.Satisfies, os.IsNotExist) + } + + for i, test := range atomicWriteFileTests { + c.Logf("test %d: %s", i, test.summary) + // First - test with file not already there. + assertDirContents() + assertNotExist(path) + contents := []byte("some\ncontents") + + err := test.change(path, contents) + if test.expectErr == "" { + c.Assert(err, gc.IsNil) + data, err := ioutil.ReadFile(path) + c.Assert(err, gc.IsNil) + c.Assert(data, jc.DeepEquals, contents) + assertDirContents(name) + } else { + c.Assert(err, gc.ErrorMatches, test.expectErr) + assertDirContents() + continue + } + + // Second - test with a file already there. + contents = []byte("new\ncontents") + err = test.change(path, contents) + c.Assert(err, gc.IsNil) + data, err = ioutil.ReadFile(path) + c.Assert(err, gc.IsNil) + c.Assert(data, jc.DeepEquals, contents) + assertDirContents(name) + + // Remove the file to reset scenario. + c.Assert(os.Remove(path), gc.IsNil) + } +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/file_unix.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/file_unix.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/file_unix.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/file_unix.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,8 +8,9 @@ "os" ) -// Replace atomically replaces the destination file or directory with the source. -// The errors that are returned are identical to those returned by os.Rename. +// ReplaceFile atomically replaces the destination file or directory +// with the source. The errors that are returned are identical to +// those returned by os.Rename. func ReplaceFile(source, destination string) error { return os.Rename(source, destination) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/fslock/fslock.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/fslock/fslock.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/fslock/fslock.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/fslock/fslock.go 2014-03-20 12:52:38.000000000 +0000 @@ -20,7 +20,7 @@ "regexp" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/gomaxprocs.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/gomaxprocs.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/gomaxprocs.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/gomaxprocs.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,26 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package utils + +import ( + "os" + "runtime" +) + +var gomaxprocs = runtime.GOMAXPROCS +var numCPU = runtime.NumCPU + +// UseMultipleCPUs sets GOMAXPROCS to the number of CPU cores unless it has +// already been overridden by the GOMAXPROCS environment variable. +func UseMultipleCPUs() { + if envGOMAXPROCS := os.Getenv("GOMAXPROCS"); envGOMAXPROCS != "" { + n := gomaxprocs(0) + logger.Debugf("GOMAXPROCS already set in environment to %q, %d internally", + envGOMAXPROCS, n) + return + } + n := numCPU() + logger.Debugf("setting GOMAXPROCS to %d", n) + gomaxprocs(n) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/gomaxprocs_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/gomaxprocs_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/gomaxprocs_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/gomaxprocs_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,51 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package utils_test + +import ( + "os" + + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" +) + +type gomaxprocsSuite struct { + testbase.LoggingSuite + setmaxprocs chan int + numCPUResponse int + setMaxProcs int +} + +var _ = gc.Suite(&gomaxprocsSuite{}) + +func (s *gomaxprocsSuite) SetUpTest(c *gc.C) { + s.LoggingSuite.SetUpTest(c) + // always stub out GOMAXPROCS so we don't actually change anything + s.numCPUResponse = 2 + s.setMaxProcs = -1 + maxProcsFunc := func(n int) int { + s.setMaxProcs = n + return 1 + } + numCPUFunc := func() int { return s.numCPUResponse } + s.PatchValue(utils.GOMAXPROCS, maxProcsFunc) + s.PatchValue(utils.NumCPU, numCPUFunc) + s.PatchEnvironment("GOMAXPROCS", "") +} + +func (s *gomaxprocsSuite) TestUseMultipleCPUsDoesNothingWhenGOMAXPROCSSet(c *gc.C) { + os.Setenv("GOMAXPROCS", "1") + utils.UseMultipleCPUs() + c.Check(s.setMaxProcs, gc.Equals, 0) +} + +func (s *gomaxprocsSuite) TestUseMultipleCPUsWhenEnabled(c *gc.C) { + utils.UseMultipleCPUs() + c.Check(s.setMaxProcs, gc.Equals, 2) + s.numCPUResponse = 4 + utils.UseMultipleCPUs() + c.Check(s.setMaxProcs, gc.Equals, 4) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/network.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/network.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/network.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/network.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,7 +7,7 @@ "fmt" "net" - "github.com/loggo/loggo" + "github.com/juju/loggo" ) var logger = loggo.GetLogger("juju.utils") diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/parallel/try_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/parallel/try_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/parallel/try_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/parallel/try_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,10 +11,10 @@ "sync" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils/parallel" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/password_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/password_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/password_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/password_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,9 +4,9 @@ package utils_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/package_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/package_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/package_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/package_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,14 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package shell_test + +import ( + "testing" + + gc "launchpad.net/gocheck" +) + +func Test(t *testing.T) { + gc.TestingT(t) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/script.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/script.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/script.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/script.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,28 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package shell + +import ( + "fmt" + + "launchpad.net/juju-core/utils" +) + +// DumpFileOnErrorScript returns a bash script that +// may be used to dump the contents of the specified +// file to stderr when the shell exits with an error. +func DumpFileOnErrorScript(filename string) string { + script := ` +dump_file() { + code=$? + if [ $code -ne 0 -a -e %s ]; then + cat %s >&2 + fi + exit $code +} +trap dump_file EXIT +`[1:] + filename = utils.ShQuote(filename) + return fmt.Sprintf(script, filename, filename) +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/script_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/script_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/shell/script_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/shell/script_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -0,0 +1,71 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package shell_test + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils/shell" +) + +type scriptSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&scriptSuite{}) + +func (*scriptSuite) TestDumpFileOnErrorScriptOutput(c *gc.C) { + script := shell.DumpFileOnErrorScript("a b c") + c.Assert(script, gc.Equals, ` +dump_file() { + code=$? + if [ $code -ne 0 -a -e 'a b c' ]; then + cat 'a b c' >&2 + fi + exit $code +} +trap dump_file EXIT +`[1:]) +} + +func (*scriptSuite) TestDumpFileOnErrorScript(c *gc.C) { + tempdir := c.MkDir() + filename := filepath.Join(tempdir, "log.txt") + err := ioutil.WriteFile(filename, []byte("abc"), 0644) + c.Assert(err, gc.IsNil) + + dumpScript := shell.DumpFileOnErrorScript(filename) + c.Logf("%s", dumpScript) + run := func(command string) (stdout, stderr string) { + var stdoutBuf, stderrBuf bytes.Buffer + cmd := exec.Command("/bin/bash", "-s") + cmd.Stdin = strings.NewReader(dumpScript + command) + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + cmd.Run() + return stdoutBuf.String(), stderrBuf.String() + } + + stdout, stderr := run("exit 0") + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "") + + stdout, stderr = run("exit 1") + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "abc") + + err = os.Remove(filename) + c.Assert(err, gc.IsNil) + stdout, stderr = run("exit 1") + c.Assert(stdout, gc.Equals, "") + c.Assert(stderr, gc.Equals, "") +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/authorisedkeys.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/authorisedkeys.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/authorisedkeys.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/authorisedkeys.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "sync" "code.google.com/p/go.crypto/ssh" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/utils" ) @@ -114,22 +114,16 @@ if err == nil { perms = info.Mode().Perm() } - // Write the data to a temp file - tempDir, err := ioutil.TempDir(keyDir, "") - if err != nil { - return err - } - tempFile := filepath.Join(tempDir, "newkeyfile") - defer os.RemoveAll(tempDir) - err = ioutil.WriteFile(tempFile, []byte(keyData), perms) + + logger.Debugf("writing authorised keys file %s", sshKeyFile) + err = utils.AtomicWriteFile(sshKeyFile, []byte(keyData), perms) if err != nil { return err } - // Rename temp file to the final location and ensure its owner - // is set correctly. - logger.Debugf("writing authorised keys file %s", sshKeyFile) // TODO (wallyworld) - what to do on windows (if anything) + // TODO(dimitern) - no need to use user.Current() if username + // is "" - it will use the current user anyway. if runtime.GOOS != "windows" { // Ensure the resulting authorised keys file has its ownership // set to the specified username. @@ -151,12 +145,12 @@ if err != nil { return err } - err = os.Chown(tempFile, uid, gid) + err = os.Chown(sshKeyFile, uid, gid) if err != nil { return err } } - return os.Rename(tempFile, sshKeyFile) + return nil } // We need a mutex because updates to the authorised keys file are done by diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/clientkeys_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/clientkeys_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/clientkeys_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/clientkeys_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,10 +7,10 @@ "io/ioutil" "os" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/ssh" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/generate_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/generate_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/generate_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/generate_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,9 +4,9 @@ package ssh_test import ( + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/ssh" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/run_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/run_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/run_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/run_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,10 +7,10 @@ "io/ioutil" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/ssh" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh.go 2014-03-20 12:52:38.000000000 +0000 @@ -19,6 +19,9 @@ // Options is a client-implementation independent SSH options set. type Options struct { + // proxyCommand specifies the command to + // execute to proxy SSH traffic through. + proxyCommand []string // ssh server port; zero means use the default (22) port int // no PTY forced by default @@ -31,6 +34,11 @@ identities []string } +// SetProxyCommand sets a command to execute to proxy traffic through. +func (o *Options) SetProxyCommand(command ...string) { + o.proxyCommand = append([]string{}, command...) +} + // SetPort sets the SSH server port to connect to. func (o *Options) SetPort(port int) { o.port = port diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh_gocrypto_test.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_gocrypto_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh_gocrypto_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_gocrypto_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,9 +10,9 @@ "sync" cryptossh "code.google.com/p/go.crypto/ssh" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/ssh" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go 2014-03-20 12:52:38.000000000 +0000 @@ -71,6 +71,9 @@ if options == nil { options = &Options{} } + if len(options.proxyCommand) > 0 { + args["-o"] = append(args["-o"], "ProxyCommand "+utils.CommandString(options.proxyCommand...)) + } if !options.passwordAuthAllowed { args["-o"] = append(args["-o"], "PasswordAuthentication no") } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/testing/fakessh.go juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/testing/fakessh.go --- juju-core-1.17.4/src/launchpad.net/juju-core/utils/ssh/testing/fakessh.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/testing/fakessh.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - "fmt" - "io/ioutil" - "path/filepath" - - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/testing/testbase" -) - -// sshscript should only print the result on the first execution, -// to handle the case where it's called multiple times. On -// subsequent executions, it should find the next 'ssh' in $PATH -// and exec that. -var sshscript = `#!/bin/bash --norc -if [ ! -e "$0.run" ]; then - touch "$0.run" - if [ -e "$0.expected-input" ]; then - diff "$0.expected-input" - - exitcode=$? - if [ $exitcode -ne 0 ]; then - echo "ERROR: did not match expected input" >&2 - exit $exitcode - fi - else - head >/dev/null - fi - # stdout - %s - # stderr - %s - exit %d -else - export PATH=${PATH#*:} - exec ssh $* -fi` - -// InstallFakeSSH creates a fake "ssh" command in a new $PATH, -// updates $PATH, and returns a function to reset $PATH to its -// original value when called. -// -// input may be: -// - nil (ignore input) -// - a string (match input exactly) -// output may be: -// - nil (no output) -// - a string (stdout) -// - a slice of strings, of length two (stdout, stderr) -func InstallFakeSSH(c *gc.C, input, output interface{}, rc int) testbase.Restorer { - fakebin := c.MkDir() - ssh := filepath.Join(fakebin, "ssh") - switch input := input.(type) { - case nil: - case string: - sshexpectedinput := ssh + ".expected-input" - err := ioutil.WriteFile(sshexpectedinput, []byte(input), 0644) - c.Assert(err, gc.IsNil) - default: - c.Errorf("input has invalid type: %T", input) - } - var stdout, stderr string - switch output := output.(type) { - case nil: - case string: - stdout = fmt.Sprintf("cat<&2< %s", i, test.errPattern, test.expectErr) resetErrors() @@ -207,7 +207,7 @@ count++ return errors.New("sample") }) - testbase.PatchValue(&retryInterval, 5*time.Millisecond) + s.PatchValue(&retryInterval, 5*time.Millisecond) w := newWorker(st) defer func() { c.Check(worker.Stop(w), gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/container_initialisation.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/container_initialisation.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/container_initialisation.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/container_initialisation.go 2014-03-20 12:52:38.000000000 +0000 @@ -144,8 +144,16 @@ var broker environs.InstanceBroker switch containerType { case instance.LXC: - initialiser = lxc.NewContainerInitialiser() - broker = NewLxcBroker(cs.provisioner, tools, cs.config) + series, err := cs.machine.Series() + if err != nil { + return nil, nil, err + } + + initialiser = lxc.NewContainerInitialiser(series) + broker, err = NewLxcBroker(cs.provisioner, tools, cs.config) + if err != nil { + return nil, nil, err + } case instance.KVM: initialiser = kvm.NewContainerInitialiser() broker, err = NewKvmBroker(cs.provisioner, tools, cs.config) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/container_initialisation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/container_initialisation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/container_initialisation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/container_initialisation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -7,6 +7,7 @@ "fmt" "os/exec" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -15,8 +16,6 @@ "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" apiprovisioner "launchpad.net/juju-core/state/api/provisioner" - jc "launchpad.net/juju-core/testing/checkers" - "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker" @@ -51,9 +50,8 @@ func (s *ContainerSetupSuite) SetUpTest(c *gc.C) { s.CommonProvisionerSuite.SetUpTest(c) s.CommonProvisionerSuite.setupEnvironmentManager(c) - aptCmdChan, cleanup := testbase.HookCommandOutput(&utils.AptCommandOutput, []byte{}, nil) + aptCmdChan := s.HookCommandOutput(&utils.AptCommandOutput, []byte{}, nil) s.aptCmdChan = aptCmdChan - s.AddCleanup(func(*gc.C) { cleanup() }) // Set up provisioner for the state machine. agentConfig := s.AgentConfigForTag(c, "machine-0") @@ -182,7 +180,7 @@ ctype instance.ContainerType packages []string }{ - {instance.LXC, []string{"lxc"}}, + {instance.LXC, []string{"--target-release", "precise-updates/cloud-tools", "lxc"}}, {instance.KVM, []string{"uvtool-libvirt", "uvtool"}}, } { s.assertContainerInitialised(c, test.ctype, test.packages) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/kvm-broker.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/kvm-broker.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/kvm-broker.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/kvm-broker.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,14 +4,12 @@ package provisioner import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/agent" - "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/container" "launchpad.net/juju-core/container/kvm" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/tools" ) @@ -26,7 +24,7 @@ tools *tools.Tools, agentConfig agent.Config, ) (environs.InstanceBroker, error) { - manager, err := kvm.NewContainerManager(container.ManagerConfig{Name: "juju"}) + manager, err := kvm.NewContainerManager(container.ManagerConfig{container.ConfigName: "juju"}) if err != nil { return nil, err } @@ -50,14 +48,10 @@ } // StartInstance is specified in the Broker interface. -func (broker *kvmBroker) StartInstance( - cons constraints.Value, - possibleTools tools.List, - machineConfig *cloudinit.MachineConfig, -) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (broker *kvmBroker) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { // TODO: refactor common code out of the container brokers. - machineId := machineConfig.MachineId + machineId := args.MachineConfig.MachineId kvmLogger.Infof("starting kvm container for machineId: %s", machineId) // TODO: Default to using the host network until we can configure. Yes, @@ -70,9 +64,9 @@ network := container.BridgeNetworkConfig(bridgeDevice) // TODO: series doesn't necessarily need to be the same as the host. - series := possibleTools.OneSeries() - machineConfig.MachineContainerType = instance.KVM - machineConfig.Tools = possibleTools[0] + series := args.Tools.OneSeries() + args.MachineConfig.MachineContainerType = instance.KVM + args.MachineConfig.Tools = args.Tools[0] config, err := broker.api.ContainerConfig() if err != nil { @@ -80,7 +74,7 @@ return nil, nil, err } if err := environs.PopulateMachineConfig( - machineConfig, + args.MachineConfig, config.ProviderType, config.AuthorizedKeys, config.SSLHostnameVerification, @@ -91,7 +85,7 @@ return nil, nil, err } - inst, hardware, err := broker.manager.StartContainer(machineConfig, series, network) + inst, hardware, err := broker.manager.CreateContainer(args.MachineConfig, series, network) if err != nil { kvmLogger.Errorf("failed to start container: %v", err) return nil, nil, err @@ -105,7 +99,7 @@ // TODO: potentially parallelise. for _, instance := range instances { kvmLogger.Infof("stopping kvm container for instance: %s", instance.Id()) - if err := broker.manager.StopContainer(instance); err != nil { + if err := broker.manager.DestroyContainer(instance); err != nil { kvmLogger.Errorf("container did not stop: %v", err) return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/kvm-broker_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/kvm-broker_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/kvm-broker_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/kvm-broker_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "path/filepath" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -22,7 +23,6 @@ "launchpad.net/juju-core/names" "launchpad.net/juju-core/state" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/provisioner" @@ -86,7 +86,11 @@ machineConfig := environs.NewMachineConfig(machineId, machineNonce, stateInfo, apiInfo) cons := constraints.Value{} possibleTools := s.broker.(coretools.HasTools).Tools() - kvm, _, err := s.broker.StartInstance(cons, possibleTools, machineConfig) + kvm, _, err := s.broker.StartInstance(environs.StartInstanceParams{ + Constraints: cons, + Tools: possibleTools, + MachineConfig: machineConfig, + }) c.Assert(err, gc.IsNil) return kvm } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/lxc-broker.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/lxc-broker.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/lxc-broker.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/lxc-broker.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,14 +4,12 @@ package provisioner import ( - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/agent" - "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/container" "launchpad.net/juju-core/container/lxc" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/tools" @@ -26,13 +24,17 @@ ContainerConfig() (params.ContainerConfig, error) } -func NewLxcBroker(api APICalls, tools *tools.Tools, agentConfig agent.Config) environs.InstanceBroker { +func NewLxcBroker(api APICalls, tools *tools.Tools, agentConfig agent.Config) (environs.InstanceBroker, error) { + manager, err := lxc.NewContainerManager(container.ManagerConfig{container.ConfigName: "juju"}) + if err != nil { + return nil, err + } return &lxcBroker{ - manager: lxc.NewContainerManager(container.ManagerConfig{Name: "juju"}), + manager: manager, api: api, tools: tools, agentConfig: agentConfig, - } + }, nil } type lxcBroker struct { @@ -47,10 +49,9 @@ } // StartInstance is specified in the Broker interface. -func (broker *lxcBroker) StartInstance(cons constraints.Value, possibleTools tools.List, - machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { +func (broker *lxcBroker) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { // TODO: refactor common code out of the container brokers. - machineId := machineConfig.MachineId + machineId := args.MachineConfig.MachineId lxcLogger.Infof("starting lxc container for machineId: %s", machineId) // Default to using the host network until we can configure. @@ -60,9 +61,9 @@ } network := container.BridgeNetworkConfig(bridgeDevice) - series := possibleTools.OneSeries() - machineConfig.MachineContainerType = instance.LXC - machineConfig.Tools = possibleTools[0] + series := args.Tools.OneSeries() + args.MachineConfig.MachineContainerType = instance.LXC + args.MachineConfig.Tools = args.Tools[0] config, err := broker.api.ContainerConfig() if err != nil { @@ -70,7 +71,7 @@ return nil, nil, err } if err := environs.PopulateMachineConfig( - machineConfig, + args.MachineConfig, config.ProviderType, config.AuthorizedKeys, config.SSLHostnameVerification, @@ -81,7 +82,7 @@ return nil, nil, err } - inst, hardware, err := broker.manager.StartContainer(machineConfig, series, network) + inst, hardware, err := broker.manager.CreateContainer(args.MachineConfig, series, network) if err != nil { lxcLogger.Errorf("failed to start container: %v", err) return nil, nil, err @@ -95,7 +96,7 @@ // TODO: potentially parallelise. for _, instance := range instances { lxcLogger.Infof("stopping lxc container for instance: %s", instance.Id()) - if err := broker.manager.StopContainer(instance); err != nil { + if err := broker.manager.DestroyContainer(instance); err != nil { lxcLogger.Errorf("container did not stop: %v", err) return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/lxc-broker_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/lxc-broker_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/lxc-broker_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/lxc-broker_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ "path/filepath" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/agent" @@ -24,7 +25,6 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/provisioner" @@ -77,7 +77,8 @@ CACert: []byte(coretesting.CACert), }) c.Assert(err, gc.IsNil) - s.broker = provisioner.NewLxcBroker(&fakeAPI{}, tools, s.agentConfig) + s.broker, err = provisioner.NewLxcBroker(&fakeAPI{}, tools, s.agentConfig) + c.Assert(err, gc.IsNil) } func (s *lxcBrokerSuite) startInstance(c *gc.C, machineId string) instance.Instance { @@ -87,7 +88,11 @@ machineConfig := environs.NewMachineConfig(machineId, machineNonce, stateInfo, apiInfo) cons := constraints.Value{} possibleTools := s.broker.(coretools.HasTools).Tools() - lxc, _, err := s.broker.StartInstance(cons, possibleTools, machineConfig) + lxc, _, err := s.broker.StartInstance(environs.StartInstanceParams{ + Constraints: cons, + Tools: possibleTools, + MachineConfig: machineConfig, + }) c.Assert(err, gc.IsNil) return lxc } @@ -203,6 +208,8 @@ func (s *lxcProvisionerSuite) expectStarted(c *gc.C, machine *state.Machine) string { s.State.StartSync() event := <-s.events + c.Assert(event.Action, gc.Equals, mock.Created) + event = <-s.events c.Assert(event.Action, gc.Equals, mock.Started) err := machine.Refresh() c.Assert(err, gc.IsNil) @@ -214,6 +221,8 @@ s.State.StartSync() event := <-s.events c.Assert(event.Action, gc.Equals, mock.Stopped) + event = <-s.events + c.Assert(event.Action, gc.Equals, mock.Destroyed) c.Assert(event.InstanceId, gc.Equals, instId) } @@ -237,7 +246,8 @@ agentConfig := s.AgentConfigForTag(c, parentMachineTag) tools, err := s.provisioner.Tools(agentConfig.Tag()) c.Assert(err, gc.IsNil) - broker := provisioner.NewLxcBroker(s.provisioner, tools, agentConfig) + broker, err := provisioner.NewLxcBroker(s.provisioner, tools, agentConfig) + c.Assert(err, gc.IsNil) return provisioner.NewContainerProvisioner(instance.LXC, s.provisioner, agentConfig, broker) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "sync" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/agent" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go 2014-03-20 12:52:38.000000000 +0000 @@ -395,7 +395,11 @@ if err != nil { return err } - inst, metadata, err := task.broker.StartInstance(cons, possibleTools, machineConfig) + inst, metadata, err := task.broker.StartInstance(environs.StartInstanceParams{ + Constraints: cons, + Tools: possibleTools, + MachineConfig: machineConfig, + }) if err != nil { // Set the state to error, so the machine will be skipped next // time until the error is resolved, but don't return an diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,6 +8,7 @@ "strings" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -23,7 +24,6 @@ "launchpad.net/juju-core/state/api/params" apiprovisioner "launchpad.net/juju-core/state/api/provisioner" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/set" "launchpad.net/juju-core/worker/provisioner" @@ -96,11 +96,8 @@ // that causes the given environMethod of the dummy provider to return // an error, which is also returned as a message to be checked. func breakDummyProvider(c *gc.C, st *state.State, environMethod string) string { - oldCfg, err := st.EnvironConfig() - c.Assert(err, gc.IsNil) - cfg, err := oldCfg.Apply(map[string]interface{}{"broken": environMethod}) - c.Assert(err, gc.IsNil) - err = st.SetEnvironConfig(cfg, oldCfg) + attrs := map[string]interface{}{"broken": environMethod} + err := st.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) return fmt.Sprintf("dummy.%s is broken", environMethod) } @@ -121,21 +118,21 @@ // so the Settings returned from the watcher will not pass // validation. func (s *CommonProvisionerSuite) invalidateEnvironment(c *gc.C) { - attrs := s.cfg.AllAttrs() - attrs["type"] = "unknown" - invalidCfg, err := config.New(config.NoDefaults, attrs) + st, err := state.Open(s.StateInfo(c), state.DefaultDialOpts(), state.Policy(nil)) c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(invalidCfg, s.cfg) + defer st.Close() + attrs := map[string]interface{}{"type": "unknown"} + err = st.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) } // fixEnvironment undoes the work of invalidateEnvironment. -func (s *CommonProvisionerSuite) fixEnvironment() error { - cfg, err := s.State.EnvironConfig() - if err != nil { - return err - } - return s.State.SetEnvironConfig(s.cfg, cfg) +func (s *CommonProvisionerSuite) fixEnvironment(c *gc.C) error { + st, err := state.Open(s.StateInfo(c), state.DefaultDialOpts(), state.Policy(nil)) + c.Assert(err, gc.IsNil) + defer st.Close() + attrs := map[string]interface{}{"type": s.cfg.AllAttrs()["type"]} + return st.UpdateEnvironConfig(attrs, nil, nil) } // stopper is stoppable. @@ -415,7 +412,7 @@ } // Unbreak the environ config. - err = s.fixEnvironment() + err = s.fixEnvironment(c) c.Assert(err, gc.IsNil) // Restart the PA to make sure the machine is skipped again. @@ -480,7 +477,7 @@ // the PA should not create it s.checkNoOperations(c) - err = s.fixEnvironment() + err = s.fixEnvironment(c) c.Assert(err, gc.IsNil) s.checkStartInstance(c, m) @@ -611,20 +608,15 @@ // the PA should create it using the old environment s.checkStartInstance(c, m) - err = s.fixEnvironment() + err = s.fixEnvironment(c) c.Assert(err, gc.IsNil) // insert our observer cfgObserver := make(chan *config.Config, 1) provisioner.SetObserver(p, cfgObserver) - oldcfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - attrs := oldcfg.AllAttrs() - attrs["secret"] = "beef" - cfg, err := config.New(config.NoDefaults, attrs) + err = s.State.UpdateEnvironConfig(map[string]interface{}{"secret": "beef"}, nil, nil) c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) s.BackingState.StartSync() @@ -666,13 +658,9 @@ c.Assert(m1.Remove(), gc.IsNil) // turn on safe mode - oldcfg, err := s.State.EnvironConfig() + attrs := map[string]interface{}{"provisioner-safe-mode": true} + err = s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) - attrs := oldcfg.AllAttrs() - attrs["provisioner-safe-mode"] = true - cfg, err := config.New(config.NoDefaults, attrs) - c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) // start a new provisioner to shut down only the machine still in state. p = s.newEnvironProvisioner(c) @@ -712,13 +700,9 @@ provisioner.SetObserver(p, cfgObserver) // turn on safe mode - oldcfg, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - attrs := oldcfg.AllAttrs() - attrs["provisioner-safe-mode"] = true - cfg, err := config.New(config.NoDefaults, attrs) + attrs := map[string]interface{}{"provisioner-safe-mode": true} + err = s.State.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) - err = s.State.SetEnvironConfig(cfg, oldcfg) s.BackingState.StartSync() diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/resumer/resumer_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/resumer/resumer_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/resumer/resumer_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/resumer/resumer_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -54,7 +54,7 @@ diff := tr.timestamps[i].Sub(tr.timestamps[i-1]) c.Assert(diff >= testInterval, gc.Equals, true) - c.Assert(diff <= 2*testInterval, gc.Equals, true) + c.Assert(diff <= 4*testInterval, gc.Equals, true) } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/rsyslog/rsyslog_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/rsyslog/rsyslog_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/rsyslog/rsyslog_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/rsyslog/rsyslog_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,16 +10,16 @@ stdtesting "testing" "time" + "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cert" - "launchpad.net/juju-core/juju/testing" + jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/log/syslog" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" - "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/worker/rsyslog" ) @@ -28,14 +28,16 @@ } type RsyslogSuite struct { - testing.JujuConnSuite + jujutesting.JujuConnSuite } var _ = gc.Suite(&RsyslogSuite{}) func (s *RsyslogSuite) SetUpSuite(c *gc.C) { s.JujuConnSuite.SetUpSuite(c) - restore := testbase.PatchValue(rsyslog.LookupUser, func(username string) (uid, gid int, err error) { + // TODO(waigani) 2014-03-19 bug 1294462 + // Add patch for suite functions + restore := testing.PatchValue(rsyslog.LookupUser, func(username string) (uid, gid int, err error) { // worker will not attempt to chown files if uid/gid is 0 return 0, 0, nil }) @@ -120,8 +122,7 @@ c.Assert(err, gc.IsNil) syslogPort := s.Conn.Environ.Config().SyslogPort() - syslogConfig := syslog.NewForwardConfig(m.Tag(), syslogPort, "", addrs) - syslogConfig.LogDir = *rsyslog.LogDir + syslogConfig := syslog.NewForwardConfig(m.Tag(), *rsyslog.LogDir, syslogPort, "", addrs) syslogConfig.ConfigDir = *rsyslog.RsyslogConfDir rendered, err := syslogConfig.Render() c.Assert(err, gc.IsNil) @@ -154,8 +155,7 @@ c.Assert(err, gc.IsNil) syslogPort := s.Conn.Environ.Config().SyslogPort() - syslogConfig := syslog.NewAccumulateConfig(m.Tag(), syslogPort, "") - syslogConfig.LogDir = *rsyslog.LogDir + syslogConfig := syslog.NewAccumulateConfig(m.Tag(), *rsyslog.LogDir, syslogPort, "") syslogConfig.ConfigDir = *rsyslog.RsyslogConfDir rendered, err := syslogConfig.Render() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/rsyslog/worker.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/rsyslog/worker.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/rsyslog/worker.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/rsyslog/worker.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,20 +6,21 @@ import ( "bytes" "fmt" - "io/ioutil" "os" "os/user" "strconv" "time" "github.com/errgo/errgo" - "github.com/loggo/loggo" + "github.com/juju/loggo" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/cert" "launchpad.net/juju-core/log/syslog" "launchpad.net/juju-core/names" apirsyslog "launchpad.net/juju-core/state/api/rsyslog" "launchpad.net/juju-core/state/api/watcher" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/worker" ) @@ -27,7 +28,7 @@ var ( rsyslogConfDir = "/etc/rsyslog.d" - logDir = "/var/log/juju" + logDir = agent.DefaultLogDir ) // RsyslogMode describes how to configure rsyslog. @@ -72,15 +73,20 @@ if err != nil { return nil, err } + logger.Debugf("starting rsyslog worker mode %v for %q %q", mode, tag, namespace) return worker.NewNotifyWorker(handler), nil } func newRsyslogConfigHandler(st *apirsyslog.State, mode RsyslogMode, tag, namespace string, stateServerAddrs []string) (*RsyslogConfigHandler, error) { var syslogConfig *syslog.SyslogConfig if mode == RsyslogModeAccumulate { - syslogConfig = syslog.NewAccumulateConfig(tag, 0, namespace) + syslogConfig = syslog.NewAccumulateConfig( + tag, logDir, 0, namespace, + ) } else { - syslogConfig = syslog.NewForwardConfig(tag, 0, namespace, stateServerAddrs) + syslogConfig = syslog.NewForwardConfig( + tag, logDir, 0, namespace, stateServerAddrs, + ) } // Historically only machine-0 includes the namespace in the log @@ -244,14 +250,16 @@ } func writeFileAtomic(path string, data []byte, mode os.FileMode, uid, gid int) error { - temp := path + ".temp" - if err := ioutil.WriteFile(temp, data, mode); err != nil { - return err - } - if uid != 0 { - if err := os.Chown(temp, uid, gid); err != nil { + chmodAndChown := func(f *os.File) error { + if err := f.Chmod(mode); err != nil { return err } + if uid != 0 { + if err := f.Chown(uid, gid); err != nil { + return err + } + } + return nil } - return os.Rename(temp, path) + return utils.AtomicWriteFileAndChange(path, data, chmodAndChown) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/stringsworker_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/stringsworker_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/stringsworker_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/stringsworker_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,13 +8,13 @@ "sync" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/tomb" apiWatcher "launchpad.net/juju-core/state/api/watcher" "launchpad.net/juju-core/state/watcher" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/worker" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/charm.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/charm.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm.go 2014-03-20 12:52:38.000000000 +0000 @@ -5,16 +5,32 @@ import ( "fmt" + "net/url" "os" "path" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/downloader" "launchpad.net/juju-core/log" - "launchpad.net/juju-core/state/api/uniter" "launchpad.net/juju-core/utils" ) +// BundleReader primarily exists to make BundlesDir mockable. +type BundleReader interface { + + // Read returns the bundle identified by the supplied info. The abort chan + // can be used to notify an implementation that it need not complete the + // operation, and can immediately error out if it is convenient to do so. + Read(bi BundleInfo, abort <-chan struct{}) (*charm.Bundle, error) +} + +// BundleInfo holds bundle information for a charm. +type BundleInfo interface { + URL() *charm.URL + ArchiveURL() (*url.URL, bool, error) + ArchiveSha256() (string, error) +} + // BundlesDir is responsible for storing and retrieving charm bundles // identified by state charms. type BundlesDir struct { @@ -29,12 +45,12 @@ // Read returns a charm bundle from the directory. If no bundle exists yet, // one will be downloaded and validated and copied into the directory before // being returned. Downloads will be aborted if a value is received on abort. -func (d *BundlesDir) Read(sch *uniter.Charm, abort <-chan struct{}) (*charm.Bundle, error) { - path := d.bundlePath(sch) +func (d *BundlesDir) Read(info BundleInfo, abort <-chan struct{}) (*charm.Bundle, error) { + path := d.bundlePath(info) if _, err := os.Stat(path); err != nil { if !os.IsNotExist(err) { return nil, err - } else if err = d.download(sch, abort); err != nil { + } else if err = d.download(info, abort); err != nil { return nil, err } } @@ -44,18 +60,18 @@ // download fetches the supplied charm and checks that it has the correct sha256 // hash, then copies it into the directory. If a value is received on abort, the // download will be stopped. -func (d *BundlesDir) download(sch *uniter.Charm, abort <-chan struct{}) (err error) { - archiveURL, disableSSLHostnameVerification, err := sch.ArchiveURL() +func (d *BundlesDir) download(info BundleInfo, abort <-chan struct{}) (err error) { + archiveURL, disableSSLHostnameVerification, err := info.ArchiveURL() if err != nil { return err } - defer utils.ErrorContextf(&err, "failed to download charm %q from %q", sch.URL(), archiveURL) + defer utils.ErrorContextf(&err, "failed to download charm %q from %q", info.URL(), archiveURL) dir := d.downloadsPath() if err := os.MkdirAll(dir, 0755); err != nil { return err } aurl := archiveURL.String() - log.Infof("worker/uniter/charm: downloading %s from %s", sch.URL(), aurl) + log.Infof("worker/uniter/charm: downloading %s from %s", info.URL(), aurl) if disableSSLHostnameVerification { log.Infof("worker/uniter/charm: SSL hostname verification disabled") } @@ -76,7 +92,7 @@ if err != nil { return err } - archiveSha256, err := sch.ArchiveSha256() + archiveSha256, err := info.ArchiveSha256() if err != nil { return err } @@ -89,15 +105,21 @@ if err := os.MkdirAll(d.path, 0755); err != nil { return err } - return os.Rename(st.File.Name(), d.bundlePath(sch)) + return os.Rename(st.File.Name(), d.bundlePath(info)) } } } // bundlePath returns the path to the location where the verified charm -// bundle identified by sch will be, or has been, saved. -func (d *BundlesDir) bundlePath(sch *uniter.Charm) string { - return path.Join(d.path, charm.Quote(sch.URL().String())) +// bundle identified by info will be, or has been, saved. +func (d *BundlesDir) bundlePath(info BundleInfo) string { + return d.bundleURLPath(info.URL()) +} + +// bundleURLPath returns the path to the location where the verified charm +// bundle identified by url will be, or has been, saved. +func (d *BundlesDir) bundleURLPath(url *charm.URL) string { + return path.Join(d.path, charm.Quote(url.String())) } // downloadsPath returns the path to the directory into which charms are diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -14,6 +14,7 @@ stdtesting "testing" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" corecharm "launchpad.net/juju-core/charm" @@ -22,7 +23,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/uniter" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/worker/uniter/charm" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,7 +10,6 @@ "path/filepath" "time" - "launchpad.net/juju-core/charm" "launchpad.net/juju-core/log" ) @@ -19,26 +18,58 @@ installPrefix = "install-" ) -// Deployer maintains a git repository tracking a series of charm versions, -// and can install and upgrade charm deployments to the current version. -type Deployer struct { - path string - current *GitDir +// Deployer is responsible for installing and upgrading charms. +type Deployer interface { + + // Stage must be called to prime the Deployer to install or upgrade the + // bundle identified by the supplied info. The abort chan can be used to + // notify an implementation that it need not complete the operation, and + // can immediately error out if it convenient to do so. It must always + // be safe to restage the same bundle, or to stage a new bundle. + Stage(info BundleInfo, abort <-chan struct{}) error + + // Deploy will install or upgrade the most recently staged bundle. + // Behaviour is undefined if Stage has not been called. + Deploy() error + + // NotifyRevert must be called when a conflicted deploy is abandoned, in + // preparation for a new upgrade. + NotifyRevert() error + + // NotifyResolved must be called when the cause of a deploy conflict has + // been resolved, and a new deploy attempt will be made. + NotifyResolved() error } -// NewDeployer creates a new Deployer which stores its state in the supplied -// directory. -func NewDeployer(path string) *Deployer { - return &Deployer{ - path: path, - current: NewGitDir(filepath.Join(path, "current")), +// gitDeployer maintains a git repository tracking a series of charm versions, +// and can install and upgrade charm deployments to the current version. +type gitDeployer struct { + target *GitDir + dataPath string + bundles BundleReader + current *GitDir +} + +// NewGitDeployer creates a new Deployer which stores its state in dataPath, +// and installs or upgrades the charm at charmPath. +func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer { + return &gitDeployer{ + target: NewGitDir(charmPath), + dataPath: dataPath, + bundles: bundles, + current: NewGitDir(filepath.Join(dataPath, "current")), } } -// Stage causes subsequent calls to Deploy to deploy the supplied charm. -func (d *Deployer) Stage(bun *charm.Bundle, url *charm.URL) error { +func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error { + // Make sure we've got an actual bundle available. + bundle, err := d.bundles.Read(info, abort) + if err != nil { + return err + } + // Read present state of current. - if err := os.MkdirAll(d.path, 0755); err != nil { + if err := os.MkdirAll(d.dataPath, 0755); err != nil { return err } defer d.collectOrphans() @@ -46,6 +77,7 @@ if err != nil { return err } + url := info.URL() if srcExists { prevURL, err := ReadCharmURL(d.current) if err != nil { @@ -74,13 +106,13 @@ } // Write the desired new state and commit. - if err = bun.ExpandTo(updatePath); err != nil { + if err = bundle.ExpandTo(updatePath); err != nil { return err } if err = WriteCharmURL(repo, url); err != nil { return err } - if err = repo.Snapshotf("Imported charm %q from %q.", url, bun.Path); err != nil { + if err = repo.Snapshotf("Imported charm %q from %q.", url, bundle.Path); err != nil { return err } @@ -92,8 +124,7 @@ return os.Rename(tmplink, d.current.Path()) } -// Deploy deploys the current charm to the target directory. -func (d *Deployer) Deploy(target *GitDir) (err error) { +func (d *gitDeployer) Deploy() (err error) { defer func() { if err == ErrConflict { log.Warningf("worker/uniter/charm: charm deployment completed with conflicts") @@ -109,17 +140,25 @@ } else if !exists { return fmt.Errorf("no charm set") } - if exists, err := target.Exists(); err != nil { + if exists, err := d.target.Exists(); err != nil { return err } else if !exists { - return d.install(target) + return d.install() } - return d.upgrade(target) + return d.upgrade() +} + +func (d *gitDeployer) NotifyRevert() error { + return d.target.Revert() +} + +func (d *gitDeployer) NotifyResolved() error { + return d.target.Snapshotf("Upgrade conflict resolved.") } // install creates a new deployment of current, and atomically moves it to // target. -func (d *Deployer) install(target *GitDir) error { +func (d *gitDeployer) install() error { defer d.collectOrphans() log.Infof("worker/uniter/charm: preparing new charm deployment") url, err := ReadCharmURL(d.current) @@ -141,60 +180,60 @@ return err } log.Infof("worker/uniter/charm: deploying charm") - return os.Rename(installPath, target.Path()) + return os.Rename(installPath, d.target.Path()) } // upgrade pulls from current into target. If target has local changes, but // no conflicts, it will be snapshotted before any changes are made. -func (d *Deployer) upgrade(target *GitDir) error { +func (d *gitDeployer) upgrade() error { log.Infof("worker/uniter/charm: preparing charm upgrade") url, err := ReadCharmURL(d.current) if err != nil { return err } - if err := target.Init(); err != nil { + if err := d.target.Init(); err != nil { return err } - if dirty, err := target.Dirty(); err != nil { + if dirty, err := d.target.Dirty(); err != nil { return err } else if dirty { - if conflicted, err := target.Conflicted(); err != nil { + if conflicted, err := d.target.Conflicted(); err != nil { return err } else if !conflicted { log.Infof("worker/uniter/charm: snapshotting dirty charm before upgrade") - if err = target.Snapshotf("Pre-upgrade snapshot."); err != nil { + if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil { return err } } } log.Infof("worker/uniter/charm: deploying charm") - if err := target.Pull(d.current); err != nil { + if err := d.target.Pull(d.current); err != nil { return err } - return target.Snapshotf("Upgraded charm to %q.", url) + return d.target.Snapshotf("Upgraded charm to %q.", url) } -// collectOrphans deletes all repos in path except the one pointed to by current. +// collectOrphans deletes all repos in dataPath except the one pointed to by current. // Errors are generally ignored; some are logged. -func (d *Deployer) collectOrphans() { +func (d *gitDeployer) collectOrphans() { current, err := os.Readlink(d.current.Path()) if err != nil { return } if !filepath.IsAbs(current) { - current = filepath.Join(d.path, current) + current = filepath.Join(d.dataPath, current) } - orphans, err := filepath.Glob(filepath.Join(d.path, fmt.Sprintf("%s*", updatePrefix))) + orphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", updatePrefix))) if err != nil { return } - installOrphans, err := filepath.Glob(filepath.Join(d.path, fmt.Sprintf("%s*", installPrefix))) + installOrphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", installPrefix))) if err != nil { return } orphans = append(orphans, installOrphans...) for _, repoPath := range orphans { - if repoPath != d.path && repoPath != current { + if repoPath != d.dataPath && repoPath != current { if err = os.RemoveAll(repoPath); err != nil { log.Warningf("worker/uniter/charm: failed to remove orphan repo at %s: %s", repoPath, err) } @@ -205,6 +244,6 @@ // newDir creates a new timestamped directory with the given prefix. It // assumes that the deployer will not need to create more than 10 // directories in any given second. -func (d *Deployer) newDir(prefix string) (string, error) { - return ioutil.TempDir(d.path, prefix+time.Now().Format("20060102-150405")) +func (d *gitDeployer) newDir(prefix string) (string, error) { + return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405")) } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,6 +4,7 @@ package charm_test import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -17,36 +18,47 @@ type DeployerSuite struct { testing.GitSuite + bundles *bundleReader + targetPath string + deployer charm.Deployer } var _ = gc.Suite(&DeployerSuite{}) +func (s *DeployerSuite) SetUpTest(c *gc.C) { + s.GitSuite.SetUpTest(c) + s.bundles = &bundleReader{} + s.targetPath = filepath.Join(c.MkDir(), "target") + deployerPath := filepath.Join(c.MkDir(), "deployer") + s.deployer = charm.NewGitDeployer(s.targetPath, deployerPath, s.bundles) +} + func (s *DeployerSuite) TestUnsetCharm(c *gc.C) { - d := charm.NewDeployer(filepath.Join(c.MkDir(), "deployer")) - err := d.Deploy(charm.NewGitDir(c.MkDir())) + err := s.deployer.Deploy() c.Assert(err, gc.ErrorMatches, "charm deployment failed: no charm set") } func (s *DeployerSuite) TestInstall(c *gc.C) { - // Install. - d := charm.NewDeployer(filepath.Join(c.MkDir(), "deployer")) - bun := bundle(c, func(path string) { + // Prepare. + info := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) c.Assert(err, gc.IsNil) }) - err := d.Stage(bun, corecharm.MustParseURL("cs:s/c-1")) + err := s.deployer.Stage(info, nil) c.Assert(err, gc.IsNil) - checkCleanup(c, d) + checkCleanup(c, s.deployer) - target := charm.NewGitDir(filepath.Join(c.MkDir(), "target")) - err = d.Deploy(target) + // Install. + err = s.deployer.Deploy() c.Assert(err, gc.IsNil) - checkCleanup(c, d) + checkCleanup(c, s.deployer) // Check content. - data, err := ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) c.Assert(err, gc.IsNil) c.Assert(string(data), gc.Equals, "hello") + + target := charm.NewGitDir(s.targetPath) url, err := charm.ReadCharmURL(target) c.Assert(err, gc.IsNil) c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-1")) @@ -59,41 +71,40 @@ func (s *DeployerSuite) TestUpgrade(c *gc.C) { // Install. - d := charm.NewDeployer(filepath.Join(c.MkDir(), "deployer")) - bun1 := bundle(c, func(path string) { + info1 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) c.Assert(err, gc.IsNil) err = os.Symlink("./some-file", filepath.Join(path, "a-symlink")) c.Assert(err, gc.IsNil) }) - err := d.Stage(bun1, corecharm.MustParseURL("cs:s/c-1")) + err := s.deployer.Stage(info1, nil) c.Assert(err, gc.IsNil) - target := charm.NewGitDir(filepath.Join(c.MkDir(), "target")) - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.IsNil) // Upgrade. - bun2 := bundle(c, func(path string) { + info2 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) c.Assert(err, gc.IsNil) err = ioutil.WriteFile(filepath.Join(path, "a-symlink"), []byte("not any more!"), 0644) c.Assert(err, gc.IsNil) }) - err = d.Stage(bun2, corecharm.MustParseURL("cs:s/c-2")) + err = s.deployer.Stage(info2, nil) c.Assert(err, gc.IsNil) - checkCleanup(c, d) - - err = d.Deploy(target) + checkCleanup(c, s.deployer) + err = s.deployer.Deploy() c.Assert(err, gc.IsNil) - checkCleanup(c, d) + checkCleanup(c, s.deployer) // Check content. - data, err := ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) c.Assert(err, gc.IsNil) c.Assert(string(data), gc.Equals, "goodbye") - data, err = ioutil.ReadFile(filepath.Join(target.Path(), "a-symlink")) + data, err = ioutil.ReadFile(filepath.Join(s.targetPath, "a-symlink")) c.Assert(err, gc.IsNil) c.Assert(string(data), gc.Equals, "not any more!") + + target := charm.NewGitDir(s.targetPath) url, err := charm.ReadCharmURL(target) c.Assert(err, gc.IsNil) c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-2")) @@ -103,43 +114,42 @@ c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Upgraded charm to "cs:s/c-2".`) } -func (s *DeployerSuite) TestConflict(c *gc.C) { +func (s *DeployerSuite) TestConflictRevertResolve(c *gc.C) { // Install. - d := charm.NewDeployer(filepath.Join(c.MkDir(), "deployer")) - bun1 := bundle(c, func(path string) { + info1 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) c.Assert(err, gc.IsNil) }) - err := d.Stage(bun1, corecharm.MustParseURL("cs:s/c-1")) + err := s.deployer.Stage(info1, nil) c.Assert(err, gc.IsNil) - target := charm.NewGitDir(filepath.Join(c.MkDir(), "target")) - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.IsNil) // Mess up target. - err = ioutil.WriteFile(filepath.Join(target.Path(), "some-file"), []byte("mu!"), 0644) + err = ioutil.WriteFile(filepath.Join(s.targetPath, "some-file"), []byte("mu!"), 0644) c.Assert(err, gc.IsNil) // Upgrade. - bun2 := bundle(c, func(path string) { + info2 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) c.Assert(err, gc.IsNil) }) - err = d.Stage(bun2, corecharm.MustParseURL("cs:s/c-2")) + err = s.deployer.Stage(info2, nil) c.Assert(err, gc.IsNil) - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.Equals, charm.ErrConflict) - checkCleanup(c, d) + checkCleanup(c, s.deployer) // Check state. + target := charm.NewGitDir(s.targetPath) conflicted, err := target.Conflicted() c.Assert(err, gc.IsNil) c.Assert(conflicted, gc.Equals, true) // Revert and check initial content. - err = target.Revert() + err = s.deployer.NotifyRevert() c.Assert(err, gc.IsNil) - data, err := ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) c.Assert(err, gc.IsNil) c.Assert(string(data), gc.Equals, "mu!") conflicted, err = target.Conflicted() @@ -147,25 +157,25 @@ c.Assert(conflicted, gc.Equals, false) // Try to upgrade again. - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.Equals, charm.ErrConflict) conflicted, err = target.Conflicted() c.Assert(err, gc.IsNil) c.Assert(conflicted, gc.Equals, true) - checkCleanup(c, d) + checkCleanup(c, s.deployer) // And again. - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.Equals, charm.ErrConflict) conflicted, err = target.Conflicted() c.Assert(err, gc.IsNil) c.Assert(conflicted, gc.Equals, true) - checkCleanup(c, d) + checkCleanup(c, s.deployer) // Manually resolve, and commit. err = ioutil.WriteFile(filepath.Join(target.Path(), "some-file"), []byte("nu!"), 0644) c.Assert(err, gc.IsNil) - err = target.Snapshotf("user resolved conflicts") + err = s.deployer.NotifyResolved() c.Assert(err, gc.IsNil) conflicted, err = target.Conflicted() c.Assert(err, gc.IsNil) @@ -173,9 +183,9 @@ // Try a final upgrade to the same charm and check it doesn't write anything // except the upgrade log line. - err = d.Deploy(target) + err = s.deployer.Deploy() c.Assert(err, gc.IsNil) - checkCleanup(c, d) + checkCleanup(c, s.deployer) data, err = ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) c.Assert(err, gc.IsNil) @@ -200,25 +210,58 @@ defer file.Close() err = dir.BundleTo(file) c.Assert(err, gc.IsNil) - bun, err := corecharm.ReadBundle(bunpath) + bundle, err := corecharm.ReadBundle(bunpath) c.Assert(err, gc.IsNil) - return bun + return bundle } -func checkCleanup(c *gc.C, d *charm.Deployer) { +func checkCleanup(c *gc.C, d charm.Deployer) { // Only one update dir should exist and be pointed to by the 'current' // symlink since extra ones should have been cleaned up by // cleanupOrphans. - updateDirs, err := filepath.Glob(filepath.Join(d.Path(), "update-*")) + deployerPath := charm.GitDeployerDataPath(d) + updateDirs, err := filepath.Glob(filepath.Join(deployerPath, "update-*")) c.Assert(err, gc.IsNil) c.Assert(updateDirs, gc.HasLen, 1) - current, err := os.Readlink(d.Current().Path()) + deployerCurrent := charm.GitDeployerCurrent(d) + current, err := os.Readlink(deployerCurrent.Path()) c.Assert(err, gc.IsNil) c.Assert(updateDirs[0], gc.Equals, current) // No install dirs should be left behind since the one created is // renamed to the target path. - installDirs, err := filepath.Glob(filepath.Join(d.Path(), "install-*")) + installDirs, err := filepath.Glob(filepath.Join(deployerPath, "install-*")) c.Assert(err, gc.IsNil) c.Assert(installDirs, gc.HasLen, 0) } + +type bundleReader struct { + bundles map[string]*corecharm.Bundle +} + +// Read implements the BundleReader interface. +func (br *bundleReader) Read(info charm.BundleInfo, abort <-chan struct{}) (*corecharm.Bundle, error) { + bundle, ok := br.bundles[info.URL().String()] + if !ok { + return nil, fmt.Errorf("no such charm!") + } + return bundle, nil +} + +func (br *bundleReader) Add(c *gc.C, url *corecharm.URL, customize func(path string)) charm.BundleInfo { + bundle := bundle(c, customize) + if br.bundles == nil { + br.bundles = map[string]*corecharm.Bundle{} + } + br.bundles[url.String()] = bundle + return &bundleInfo{nil, url} +} + +type bundleInfo struct { + charm.BundleInfo + url *corecharm.URL +} + +func (info *bundleInfo) URL() *corecharm.URL { + return info.url +} diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,11 +4,11 @@ package charm // exported so we can get the deployer path from tests. -func (d *Deployer) Path() string { - return d.path +func GitDeployerDataPath(d Deployer) string { + return d.(*gitDeployer).dataPath } // exported so we can get the deployer current git repo from tests. -func (d *Deployer) Current() *GitDir { - return d.current +func GitDeployerCurrent(d Deployer) *GitDir { + return d.(*gitDeployer).current } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,11 +9,11 @@ "os/exec" "path/filepath" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" corecharm "launchpad.net/juju-core/charm" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/worker/uniter/charm" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/context.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/context.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/context.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/context.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/charm" "launchpad.net/juju-core/juju/osenv" @@ -253,7 +253,16 @@ } func (ctx *HookContext) runCharmHook(hookName, charmDir string, env []string) error { - ps := exec.Command(filepath.Join(charmDir, "hooks", hookName)) + hook, err := exec.LookPath(filepath.Join(charmDir, "hooks", hookName)) + if err != nil { + if ee, ok := err.(*exec.Error); ok && os.IsNotExist(ee.Err) { + // Missing hook is perfectly valid, but worth mentioning. + logger.Infof("skipped %q hook (not implemented)", hookName) + return &missingHookError{hookName} + } + return err + } + ps := exec.Command(hook) ps.Env = env ps.Dir = charmDir outReader, outWriter, err := os.Pipe() @@ -274,13 +283,6 @@ err = ps.Wait() } hookLogger.stop() - if ee, ok := err.(*exec.Error); ok && err != nil { - if os.IsNotExist(ee.Err) { - // Missing hook is perfectly valid, but worth mentioning. - logger.Infof("skipped %q hook (not implemented)", hookName) - return &missingHookError{hookName} - } - } return err } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/context_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/context_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/context_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/context_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,6 +11,7 @@ "strings" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm" @@ -20,7 +21,6 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" apiuniter "launchpad.net/juju-core/state/api/uniter" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/worker/uniter" "launchpad.net/juju-core/worker/uniter/jujuc" @@ -174,10 +174,11 @@ stdout: strings.Repeat("a", lineBufferSize+10), }, }, { - summary: "check shell environment for non-relation hook context", - relid: -1, - spec: hookSpec{perm: 0700}, - proxySettings: osenv.ProxySettings{Http: "http", Https: "https", Ftp: "ftp"}, + summary: "check shell environment for non-relation hook context", + relid: -1, + spec: hookSpec{perm: 0700}, + proxySettings: osenv.ProxySettings{ + Http: "http", Https: "https", Ftp: "ftp", NoProxy: "no proxy"}, env: map[string]string{ "JUJU_UNIT_NAME": "u/0", "JUJU_API_ADDRESSES": expectedApiAddrs, @@ -188,6 +189,8 @@ "HTTPS_PROXY": "https", "ftp_proxy": "ftp", "FTP_PROXY": "ftp", + "no_proxy": "no proxy", + "NO_PROXY": "no proxy", }, }, { summary: "check shell environment for relation-broken hook context", diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/debug/server_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/debug/server_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/debug/server_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/debug/server_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -12,9 +12,9 @@ "regexp" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/filter.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/filter.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/filter.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/filter.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "sort" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/charm" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "fmt" "strings" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/gnuflag" "launchpad.net/juju-core/cmd" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/juju-log_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -6,7 +6,7 @@ import ( "fmt" - "github.com/loggo/loggo" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/server.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/server.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/server.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/server.go 2014-03-20 12:52:38.000000000 +0000 @@ -15,7 +15,7 @@ "sort" "sync" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/utils/exec" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/server_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/server_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/jujuc/server_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/jujuc/server_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,12 +13,12 @@ "sync" "time" + jc "github.com/juju/testing/checkers" "launchpad.net/gnuflag" gc "launchpad.net/gocheck" "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/exec" "launchpad.net/juju-core/worker/uniter/jujuc" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/modes.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/modes.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/modes.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/modes.go 2014-03-20 12:52:38.000000000 +0000 @@ -392,26 +392,23 @@ } u.f.WantResolvedEvent() u.f.WantUpgradeEvent(true) - for { - select { - case <-u.tomb.Dying(): - return nil, tomb.ErrDying - case <-u.f.ResolvedEvents(): - err = u.charm.Snapshotf("Upgrade conflict resolved.") - if e := u.f.ClearResolved(); e != nil { - return nil, e - } - if err != nil { - return nil, err - } - return ModeUpgrading(curl), nil - case curl := <-u.f.UpgradeEvents(): - if err := u.charm.Revert(); err != nil { - return nil, err - } - return ModeUpgrading(curl), nil + select { + case <-u.tomb.Dying(): + return nil, tomb.ErrDying + case <-u.f.ResolvedEvents(): + err = u.deployer.NotifyResolved() + if e := u.f.ClearResolved(); e != nil { + return nil, e + } + if err != nil { + return nil, err + } + case curl = <-u.f.UpgradeEvents(): + if err := u.deployer.NotifyRevert(); err != nil { + return nil, err } } + return ModeUpgrading(curl), nil } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/relation/relation_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/relation/relation_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/relation/relation_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/relation/relation_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,10 +10,10 @@ "path/filepath" "strconv" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm/hooks" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/worker/uniter/hook" "launchpad.net/juju-core/worker/uniter/relation" ) diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/relationer_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/relationer_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/relationer_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/relationer_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -10,6 +10,7 @@ "strings" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/charm/hooks" @@ -19,7 +20,6 @@ "launchpad.net/juju-core/state/api" apiuniter "launchpad.net/juju-core/state/api/uniter" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/worker/uniter" "launchpad.net/juju-core/worker/uniter/hook" diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/runlistener.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/runlistener.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/runlistener.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/runlistener.go 2014-03-20 12:52:38.000000000 +0000 @@ -9,6 +9,7 @@ import ( "net" "net/rpc" + "os" "sync" "launchpad.net/juju-core/utils/exec" @@ -49,18 +50,22 @@ return err } -// NewRunListener returns a new RunListener that is listening on the network -// type and address passed in. If a valid RunListener is returned, is has the -// go routine running, and should be closed by the creator when they are done -// with it. -func NewRunListener(runner CommandRunner, netType, localAddr string) (*RunListener, error) { +// NewRunListener returns a new RunListener that is listening on given +// unix socket path passed in. If a valid RunListener is returned, is +// has the go routine running, and should be closed by the creator +// when they are done with it. +func NewRunListener(runner CommandRunner, socketPath string) (*RunListener, error) { server := rpc.NewServer() if err := server.Register(&JujuRunServer{runner}); err != nil { return nil, err } - listener, err := net.Listen(netType, localAddr) + // In case the unix socket is present, delete it. + if err := os.Remove(socketPath); err != nil { + logger.Tracef("ignoring error on removing %q: %v", socketPath, err) + } + listener, err := net.Listen("unix", socketPath) if err != nil { - logger.Errorf("failed to listen on %s %s: %v", netType, localAddr, err) + logger.Errorf("failed to listen on unix:%s: %v", socketPath, err) return nil, err } runListener := &RunListener{ diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/runlistener_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/runlistener_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/runlistener_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/runlistener_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -24,7 +24,7 @@ // Mirror the params to uniter.NewRunListener, but add cleanup to close it. func (s *ListenerSuite) NewRunListener(c *gc.C) *uniter.RunListener { s.socketPath = filepath.Join(c.MkDir(), "test.listener") - listener, err := uniter.NewRunListener(&mockRunner{c}, "unix", s.socketPath) + listener, err := uniter.NewRunListener(&mockRunner{c}, s.socketPath) c.Assert(err, gc.IsNil) c.Assert(listener, gc.NotNil) s.AddCleanup(func(*gc.C) { @@ -33,13 +33,13 @@ return listener } -func (s *ListenerSuite) TestNewRunListenerSecondFails(c *gc.C) { +func (s *ListenerSuite) TestNewRunListenerOnExistingSocketRemovesItAndSucceeds(c *gc.C) { s.NewRunListener(c) - listener, err := uniter.NewRunListener(&mockRunner{}, "unix", s.socketPath) - - c.Assert(listener, gc.IsNil) - c.Assert(err, gc.ErrorMatches, ".* address already in use") + listener, err := uniter.NewRunListener(&mockRunner{}, s.socketPath) + c.Assert(err, gc.IsNil) + c.Assert(listener, gc.NotNil) + listener.Close() } func (s *ListenerSuite) TestClientCall(c *gc.C) { diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/uniter.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/uniter.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/uniter.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/uniter.go 2014-03-20 12:52:38.000000000 +0000 @@ -13,7 +13,7 @@ "sync" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/agent/tools" @@ -41,8 +41,7 @@ // These work fine for linux, but should we need to work with windows // workloads in the future, we'll need to move these into a file that is // compiled conditionally for different targets and use tcp (most likely). - RunListenerNetType = "unix" - RunListenerFile = "run.socket" + RunListenerFile = "run.socket" ) // A UniterExecutionObserver gets the appropriate methods called when a hook @@ -73,8 +72,7 @@ toolsDir string relationsDir string charm *charm.GitDir - bundles *charm.BundlesDir - deployer *charm.Deployer + deployer charm.Deployer s *State sf *StateFile rand *rand.Rand @@ -194,8 +192,8 @@ u.envName = env.Name() runListenerSocketPath := filepath.Join(u.baseDir, RunListenerFile) - logger.Debugf("starting juju-run listener on %s:%s", RunListenerNetType, runListenerSocketPath) - u.runListener, err = NewRunListener(u, RunListenerNetType, runListenerSocketPath) + logger.Debugf("starting juju-run listener on unix:%s", runListenerSocketPath) + u.runListener, err = NewRunListener(u, runListenerSocketPath) if err != nil { return err } @@ -206,8 +204,9 @@ u.relationers = map[int]*Relationer{} u.relationHooks = make(chan hook.Info) u.charm = charm.NewGitDir(filepath.Join(u.baseDir, "charm")) - u.bundles = charm.NewBundlesDir(filepath.Join(u.baseDir, "state", "bundles")) - u.deployer = charm.NewDeployer(filepath.Join(u.baseDir, "state", "deployer")) + deployerPath := filepath.Join(u.baseDir, "state", "deployer") + bundles := charm.NewBundlesDir(filepath.Join(u.baseDir, "state", "bundles")) + u.deployer = charm.NewGitDeployer(u.charm.Path(), deployerPath, bundles) u.sf = NewStateFile(filepath.Join(u.baseDir, "state", "uniter")) u.rand = rand.New(rand.NewSource(time.Now().Unix())) return nil @@ -269,11 +268,7 @@ if err != nil { return err } - bun, err := u.bundles.Read(sch, u.tomb.Dying()) - if err != nil { - return err - } - if err = u.deployer.Stage(bun, curl); err != nil { + if err = u.deployer.Stage(sch, u.tomb.Dying()); err != nil { return err } @@ -291,7 +286,7 @@ if err = u.writeState(reason, Pending, hi, curl); err != nil { return err } - if err = u.deployer.Deploy(u.charm); err != nil { + if err = u.deployer.Deploy(); err != nil { return err } if err = u.writeState(reason, Done, hi, curl); err != nil { @@ -505,9 +500,6 @@ delete(u.relationers, hi.RelationId) } } - if err := u.charm.Snapshotf("Completed %q hook.", hi.Kind); err != nil { - return err - } if hi.Kind == hooks.ConfigChanged { u.ranConfigChanged = true } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/uniter_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/uniter_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/uniter/uniter_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/uniter_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -17,6 +17,7 @@ stdtesting "testing" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -30,7 +31,6 @@ "launchpad.net/juju-core/state/api/params" apiuniter "launchpad.net/juju-core/state/api/uniter" coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" "launchpad.net/juju-core/utils" utilexec "launchpad.net/juju-core/utils/exec" "launchpad.net/juju-core/utils/fslock" @@ -752,7 +752,7 @@ status: params.StatusStarted, }, waitHooks{"install", "config-changed", "start"}, - verifyCharm{}, + verifyCharm{dirty: true}, createCharm{ revision: 1, @@ -873,7 +873,7 @@ ), ut( "run commands: proxy settings set", quickStartRelation{}, - setProxySettings{Http: "http", Https: "https", Ftp: "ftp"}, + setProxySettings{Http: "http", Https: "https", Ftp: "ftp", NoProxy: "localhost"}, runCommands{ fmt.Sprintf("echo $http_proxy > %s", testFile("proxy.output")), fmt.Sprintf("echo $HTTP_PROXY >> %s", testFile("proxy.output")), @@ -881,10 +881,12 @@ fmt.Sprintf("echo $HTTPS_PROXY >> %s", testFile("proxy.output")), fmt.Sprintf("echo $ftp_proxy >> %s", testFile("proxy.output")), fmt.Sprintf("echo $FTP_PROXY >> %s", testFile("proxy.output")), + fmt.Sprintf("echo $no_proxy >> %s", testFile("proxy.output")), + fmt.Sprintf("echo $NO_PROXY >> %s", testFile("proxy.output")), }, verifyFile{ testFile("proxy.output"), - "http\nhttp\nhttps\nhttps\nftp\nftp\n", + "http\nhttp\nhttps\nhttps\nftp\nftp\nlocalhost\nlocalhost\n", }, ), ut( "run commands: async using rpc client", @@ -1149,7 +1151,7 @@ if err != nil || len(addresses) == 0 { testing.AddStateServerMachine(c, ctx.st) } - addresses, err = ctx.st.APIAddresses() + addresses, err = ctx.st.APIAddressesFromMachines() c.Assert(err, gc.IsNil) c.Assert(addresses, gc.HasLen, 1) } @@ -1623,7 +1625,7 @@ status: params.StatusStarted, }, waitHooks{"install", "config-changed", "start"}, - verifyCharm{}, + verifyCharm{dirty: true}, createCharm{ revision: 1, @@ -1957,15 +1959,13 @@ type setProxySettings osenv.ProxySettings func (s setProxySettings) step(c *gc.C, ctx *context) { - old, err := ctx.st.EnvironConfig() - c.Assert(err, gc.IsNil) - cfg, err := old.Apply(map[string]interface{}{ + attrs := map[string]interface{}{ "http-proxy": s.Http, "https-proxy": s.Https, "ftp-proxy": s.Ftp, - }) - c.Assert(err, gc.IsNil) - err = ctx.st.SetEnvironConfig(cfg, old) + "no-proxy": s.NoProxy, + } + err := ctx.st.UpdateEnvironConfig(attrs, nil, nil) c.Assert(err, gc.IsNil) // wait for the new values... expected := (osenv.ProxySettings)(s) @@ -1978,6 +1978,8 @@ c.Assert(os.Getenv("HTTPS_PROXY"), gc.Equals, expected.Https) c.Assert(os.Getenv("ftp_proxy"), gc.Equals, expected.Ftp) c.Assert(os.Getenv("FTP_PROXY"), gc.Equals, expected.Ftp) + c.Assert(os.Getenv("no_proxy"), gc.Equals, expected.NoProxy) + c.Assert(os.Getenv("NO_PROXY"), gc.Equals, expected.NoProxy) return } } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/error.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/error.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/error.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/error.go 2014-03-20 12:52:38.000000000 +0000 @@ -4,7 +4,7 @@ package upgrader import ( - agenttools "launchpad.net/juju-core/agent/tools" + "launchpad.net/juju-core/agent/tools" "launchpad.net/juju-core/version" ) @@ -25,10 +25,10 @@ // It should be called just before an agent exits, so that // it will restart running the new tools. func (e *UpgradeReadyError) ChangeAgentTools() error { - tools, err := agenttools.ChangeAgentTools(e.DataDir, e.AgentName, e.NewTools) + agentTools, err := tools.ChangeAgentTools(e.DataDir, e.AgentName, e.NewTools) if err != nil { return err } - logger.Infof("upgraded from %v to %v (%q)", e.OldTools, tools.Version, tools.URL) + logger.Infof("upgraded from %v to %v (%q)", e.OldTools, agentTools.Version, agentTools.URL) return nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/upgrader.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/upgrader.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader.go 2014-03-20 12:52:38.000000000 +0000 @@ -8,7 +8,7 @@ "net/http" "time" - "github.com/loggo/loggo" + "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/agent" @@ -165,5 +165,6 @@ if err != nil { return fmt.Errorf("cannot unpack tools: %v", err) } + logger.Infof("unpacked tools %s to %s", agentTools.Version, u.dataDir) return nil } diff -Nru juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go --- juju-core-1.17.4/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go 2014-02-27 20:17:50.000000000 +0000 +++ juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go 2014-03-20 12:52:38.000000000 +0000 @@ -11,7 +11,9 @@ "time" gc "launchpad.net/gocheck" + coretesting "launchpad.net/juju-core/testing" + jc "github.com/juju/testing/checkers" "launchpad.net/juju-core/agent" agenttools "launchpad.net/juju-core/agent/tools" envtesting "launchpad.net/juju-core/environs/testing" @@ -22,8 +24,6 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" statetesting "launchpad.net/juju-core/state/testing" - coretesting "launchpad.net/juju-core/testing" - jc "launchpad.net/juju-core/testing/checkers" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/upgrader" @@ -122,9 +122,7 @@ s.PatchValue(&version.Current, oldTools.Version) newTools := envtesting.AssertUploadFakeToolsVersions( c, stor, version.MustParseBinary("5.4.5-precise-amd64"))[0] - err := envtools.MergeAndWriteMetadata(stor, coretools.List{oldTools, newTools}, envtools.DoNotWriteMirrors) - c.Assert(err, gc.IsNil) - err = statetesting.SetAgentVersion(s.State, newTools.Version.Number) + err := statetesting.SetAgentVersion(s.State, newTools.Version.Number) c.Assert(err, gc.IsNil) // Make the download take a while so that we verify that @@ -151,9 +149,7 @@ s.PatchValue(&version.Current, oldTools.Version) newTools := envtesting.AssertUploadFakeToolsVersions( c, stor, version.MustParseBinary("5.4.5-precise-amd64"))[0] - err := envtools.MergeAndWriteMetadata(stor, coretools.List{oldTools, newTools}, envtools.DoNotWriteMirrors) - c.Assert(err, gc.IsNil) - err = statetesting.SetAgentVersion(s.State, newTools.Version.Number) + err := statetesting.SetAgentVersion(s.State, newTools.Version.Number) c.Assert(err, gc.IsNil) retryc := make(chan time.Time)