├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── Vagrantfile ├── ansible.cfg ├── connection_plugins ├── __init__.py └── machinectl.py ├── contrib └── inventory │ └── machinectl.py ├── dobi.yaml ├── docker ├── .dockerignore ├── Dockerfile.base ├── Dockerfile.master ├── Dockerfile.tox ├── Dockerfile.travis ├── Dockerfile.vagrant └── files │ ├── docker.default │ ├── fuser.dummy │ ├── mkosi.tmpfiles.d.conf │ └── vagrant.sudoers.d ├── hosts ├── meta └── main.yml ├── mkosi ├── mkosi.default ├── mkosi.files │ ├── mkosi.arch │ ├── mkosi.bionic │ ├── mkosi.fedora │ ├── mkosi.stretch │ └── mkosi.xenial └── mkosi.nspawn ├── scripts ├── build-images ├── configure-docker ├── docker-run-travis-container ├── download-images ├── download-images-and-spawn-containers ├── make-ansible-executable ├── resize-machine-btrfs-partition ├── run-tests ├── spawn-containers ├── test-make-hardlinks └── wait-is-system-running ├── tests ├── files │ ├── directory │ │ ├── foo │ │ └── hello │ └── hello ├── inventory ├── test.retry └── test.yml └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Ansible retry hosts 60 | *.retry 61 | 62 | # Vagrant data 63 | /.vagrant/ 64 | 65 | # mkosi stuff 66 | /mkosi/mkosi.cache/ 67 | /mkosi/mkosi.output/ 68 | *.mkosi 69 | 70 | # dobi stuff 71 | /.dobi/ 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Shiny! 3 | dist: xenial 4 | 5 | # Use full VM, as we need the ability to run containers in --privileged mode 6 | sudo: required 7 | 8 | # Just need to be able to run Docker (plus some shell scripts); everything else 9 | # happens in containers. 10 | language: minimal 11 | 12 | env: 13 | global: 14 | - IMAGE=tomeon/fedora-mkosi:29-travis 15 | - CONTAINER="fedora-mkosi-29-travis-python-${TRAVIS_PYTHON_VERSION}" 16 | - TRAVIS_BUILD_DIR_BIND_DEST=/vagrant 17 | matrix: 18 | - ANSIBLE=2.4 19 | - ANSIBLE=2.5 20 | - ANSIBLE=2.6 21 | - ANSIBLE=2.7 22 | - ANSIBLE=2.8 23 | - ANSIBLE=2.9 24 | - ANSIBLE=2.10 25 | 26 | services: 27 | - docker 28 | 29 | before_install: 30 | - 'sudo "${TRAVIS_BUILD_DIR}/scripts/configure-docker"' 31 | - 'docker pull "$IMAGE"' 32 | - '"${TRAVIS_BUILD_DIR}/scripts/docker-run-travis-container" --name "$CONTAINER" "$IMAGE"' 33 | - 'docker exec "$CONTAINER" printenv | sort' 34 | - 'docker exec "$CONTAINER" "${TRAVIS_BUILD_DIR_BIND_DEST}/scripts/wait-is-system-running"' 35 | - 'docker exec "$CONTAINER" systemctl list-units "*machine*"' 36 | - 'docker exec "$CONTAINER" systemctl start systemd-machined' 37 | - 'docker exec "$CONTAINER" systemctl list-units "*machine*"' 38 | - 'docker exec "$CONTAINER" "${TRAVIS_BUILD_DIR_BIND_DEST}/scripts/resize-machine-btrfs-partition"' 39 | # Needed for creating a dummy TTY when running tests 40 | - 'docker exec "$CONTAINER" dnf -y install socat' 41 | 42 | install: 43 | - 'docker exec "$CONTAINER" "${TRAVIS_BUILD_DIR_BIND_DEST}/scripts/download-images-and-spawn-containers"' 44 | 45 | script: 46 | - 'docker exec "$CONTAINER" "${TRAVIS_BUILD_DIR_BIND_DEST}/scripts/run-tests" "$TRAVIS_BUILD_DIR_BIND_DEST"' 47 | 48 | notifications: 49 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `tomeon.ansible_connection_machinectl` 2 | 3 | ## [Unreleased] 4 | 5 | ### Added 6 | 7 | - Docker build assets and [dobi](https://github.com/dnephin/dobi) build 8 | specification. 9 | - Vagrant setup that mimics a stripped-down Travis build environment. 10 | - tox-based testing setup for running tests against multiple versions of Python 11 | and Ansible. 12 | - Add `machined_config` structure to hostvars, representing the output of 13 | `machinectl show`. 14 | - Tests for file transfer and deletion, and for command execution. 15 | 16 | ### Changed 17 | 18 | - Expand `ansible-galaxy`-based installation instructions, explicitly 19 | mentioning the "role" name. (#5) 20 | - Account for restructuring of Ansible's text-handling libraries by tring to 21 | import `to_bytes` and `to_native` from `ansible.module_utils._text`, falling 22 | back to importing `to_bytes` and `to_str` from `ansible.utils.unicode` (and 23 | aliasing `to_str` to `to_native`). (#4) 24 | - Open `machinectl` connection's standard input in binary mode and convert data 25 | read from standard output and standard error to the correct native Python 26 | representation via the `to_native` function. (#4) 27 | - Don't limit the number of fields returned by `str.split` when parsing the 28 | output of `machinectl list`. 29 | - Place per-container hostvars under `machine_config` key, representing the 30 | output of `machinectl show `. 31 | - Properly extract hostvars for named host when running dynamic inventory with 32 | the `--host` flag. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ansible Connection Plugin - `machinectl` 2 | ======================================== 3 | 4 | Ansible plugin that uses `systemd`'s 5 | [`machinectl`](https://www.freedesktop.org/software/systemd/man/machinectl.html) 6 | to communicate with virtual machines and containers managed by 7 | [`systemd-machined`](https://www.freedesktop.org/software/systemd/man/systemd-machined.service.html). 8 | 9 | Requirements 10 | ------------ 11 | 12 | This plugin relies on `machinectl`'s `shell` subcommand, which was introduced 13 | in `systemd` version 225; see the [release 14 | notes](https://github.com/systemd/systemd/blob/master/NEWS#L1233-L1241) 15 | ([permalink](https://github.com/systemd/systemd/blob/3dea75dead5d3b229c9780de63479ea0aa655ba5/NEWS#L1233-L1241)). 16 | 17 | Installation 18 | ------------ 19 | 20 | This plugin can be installed by running `ansible-galaxy install 21 | tomeon.ansible_connection_machinectl`, or by directly cloning this repository. 22 | You will need to move 23 | [`connection_plugins/machinectl.py`](connection_plugins/machinectl.py) into one 24 | of the `connection_plugins` directories configured in your `ansible.cfg`. 25 | 26 | Connection Variables 27 | -------------------- 28 | 29 | None at the moment. 30 | 31 | Example Usage 32 | ------------- 33 | 34 | Simply pass the `-c`|`--connection=` option to Ansible along with the machine 35 | name. Given the machine `ansible-nspawn` (in a typical setup, this would refer 36 | to a chroot located at `/var/lib/machines/ansible-nspawn`): 37 | 38 | ```sh 39 | $ ansible -c machinectl -m setup ansible-nspawn 40 | archlinux-ansible | SUCCESS => { 41 | "ansible_facts": { 42 | # 43 | "ansible_virtualization_type": "systemd-nspawn", 44 | # 45 | }, 46 | "changed": false 47 | } 48 | ``` 49 | 50 | Caveats 51 | ------- 52 | 53 | `machinectl` requires superuser privileges when running the `shell` subcommand. 54 | There's no straightforward way to piggyback on Ansible's `become` logic, as 55 | this would mean distinguishing between whether the user wants to (1) acquire 56 | superuser privileges on the control machine, or (2) acquire superuser 57 | privileges within the target machine. 58 | 59 | For the moment, this means that Ansible must be run with superuser privileges 60 | in order to use this connection type. The plan is to add connection 61 | configuration parameters for specifying the local user and their password when 62 | running `machinectl`; at the moment, do: 63 | 64 | ```sh 65 | # Preserve your environment when escalating privileges 66 | $ sudo -E ansible -c machinectl -m setup 67 | ``` 68 | 69 | License 70 | ------- 71 | 72 | BSD 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## User resolution 4 | 5 | From `local.py`: 6 | 7 | ```python 8 | # Because we haven't made any remote connection we're running as 9 | # the local user, rather than as whatever is configured in 10 | # remote_user. 11 | self._play_context.remote_user = getpass.getuser() 12 | ``` 13 | 14 | `machinectl` might need different logic because of the fact that it does allow 15 | remote connections via `ssh`. 16 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | unless Vagrant.has_plugin? 'vagrant-sshfs' 2 | abort 'Plugins must be enabled and the vagrant-sshfs plugin must be installed.' 3 | end 4 | 5 | $docker_host ||= '0.0.0.0' 6 | $docker_port ||= 6379 7 | 8 | $docker_forwarded_host ||= '127.0.0.1' 9 | $docker_forwarded_port ||= 63790 10 | 11 | $host_to_docker_daemon_conf ||= { 12 | trusty: { 13 | 'hosts' => [ 14 | "tcp://#{$docker_host}:#{$docker_port}", 15 | 'unix://' 16 | ], 17 | 'storage-driver' => 'devicemapper' 18 | }, 19 | xenial: { 20 | 'storage-driver' => 'overlay2' 21 | }, 22 | } 23 | 24 | Vagrant.configure(2) do |config| 25 | $host_to_docker_daemon_conf.sort.each_with_index do |(ubuntu_release, docker_daemon_conf), i| 26 | config.vm.define ubuntu_release do |ubuntu| 27 | ubuntu.vm.box = "ubuntu/#{ubuntu_release}64" 28 | 29 | ubuntu.vm.provider :virtualbox do |v| 30 | v.memory = 2048 31 | end 32 | 33 | ubuntu.vm.synced_folder '.', '/vagrant', disabled: true 34 | 35 | # Installs docker; does nothing else 36 | ubuntu.vm.provision :docker do |d| 37 | d.post_install_provision 'configure-docker', type: :shell do |s| 38 | require 'json' 39 | s.args = [JSON.pretty_generate(docker_daemon_conf)] 40 | s.path = 'scripts/configure-docker' 41 | end 42 | end 43 | 44 | ubuntu.vm.network :forwarded_port, guest: $docker_port, host: $docker_forwarded_port + i, host_ip: $docker_forwarded_host 45 | end 46 | 47 | config.vm.define "#{ubuntu_release}-builder" do |fedora| 48 | fedora.vm.synced_folder '.', '/vagrant', type: 'sshfs' 49 | 50 | fedora.vm.provider :docker do |d| 51 | d.image = 'tomeon/fedora-mkosi:29-vagrant' 52 | d.cmd = %w[/usr/sbin/init] 53 | d.create_args = %w[ 54 | --cap-add SYS_ADMIN 55 | --tmpfs /run:exec 56 | --tmpfs /tmp:exec 57 | -v /sys/fs/cgroup:/sys/fs/cgroup:ro 58 | ] 59 | d.force_host_vm = true 60 | d.privileged = true 61 | d.has_ssh = true 62 | d.remains_running = true 63 | d.vagrant_machine = ubuntu_release 64 | d.vagrant_vagrantfile = __FILE__ 65 | end 66 | 67 | fedora.vm.provision 'test-make-hardlinks', type: :shell do |s| 68 | s.path = 'scripts/test-make-hardlinks' 69 | s.args = %w[/vagrant] 70 | end 71 | 72 | fedora.vm.provision 'wait-is-system-running', type: :shell do |s| 73 | s.path = 'scripts/wait-is-system-running' 74 | end 75 | 76 | fedora.vm.provision 'resize-machine-btrfs-partition', type: :shell do |s| 77 | s.path = 'scripts/resize-machine-btrfs-partition' 78 | s.privileged = true 79 | end 80 | 81 | fedora.vm.provision 'build-images', type: :shell, run: :never do |s| 82 | s.path = 'scripts/build-images' 83 | s.args = %w[-a --checksum] 84 | s.privileged = true 85 | end 86 | 87 | fedora.vm.provision 'download-images', type: :shell do |s| 88 | mkosi_files = Pathname.new('mkosi/mkosi.files').expand_path(__dir__).children(false) 89 | images = mkosi_files.map(&:to_s).select { |d| d.start_with? 'mkosi.' }.map { |d| d.sub('mkosi.', '') } 90 | s.path = 'scripts/download-images' 91 | s.args = images 92 | s.privileged = true 93 | end 94 | 95 | fedora.vm.provision 'spawn-containers', type: :shell do |s| 96 | s.path = 'scripts/spawn-containers' 97 | s.privileged = true 98 | end 99 | 100 | fedora.vm.provision 'run-tests', type: :shell, run: :never do |s| 101 | s.path = 'scripts/run-tests' 102 | s.args = '/vagrant' 103 | s.privileged = true 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | connection_plugins = connection_plugins 3 | -------------------------------------------------------------------------------- /connection_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomeon/ansible-connection-machinectl/6bba4355693b4bfd0426f01753d53922aae52c21/connection_plugins/__init__.py -------------------------------------------------------------------------------- /connection_plugins/machinectl.py: -------------------------------------------------------------------------------- 1 | # Inspired by, but since deviated entirely from, the nsenter connection plugin 2 | # (c) 2015, Tomohiro NAKAMURA 3 | # Permalink: https://github.com/jptomo/ansible-connection-nsenter/blob/4ab713b061c92eaf2553a5c826cd26266e932b09/nsenter.py 4 | # 5 | # The polling loop in Connection.exec_command was adapted from local.py 6 | # (c) 2012, Michael DeHaan 7 | # (c) 2015 Toshio Kuratomi 8 | # Permalink: https://github.com/ansible/ansible/blob/a9d5bf717c200126c46433de1a833f2dd34397f6/lib/ansible/plugins/connection/ssh.py#L332-L340 9 | # 10 | # The pty.getpty() code in Connection.exec_command was adapated from ssh.py 11 | # (c) 2012, Michael DeHaan 12 | # Copyright 2015 Abhijit Menon-Sen 13 | # Permalink: https://github.com/ansible/ansible/blob/a9d5bf717c200126c46433de1a833f2dd34397f6/lib/ansible/plugins/connection/ssh.py#L332-L340 14 | # 15 | # Connection plugin for machinectl virtual machines and containers 16 | # (c) 2016, Matt Schreiber 17 | # 18 | # This machinectl connection plugin is distributed in the hope that it will be 19 | # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 21 | # Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with Ansible. If not, see . 25 | from __future__ import (absolute_import, division, print_function) 26 | __metaclass__ = type 27 | 28 | import collections 29 | import distutils.spawn 30 | import fcntl 31 | import os 32 | import pty 33 | import re 34 | import select 35 | import shlex 36 | import subprocess 37 | 38 | from ansible.errors import AnsibleError 39 | from ansible.plugins.connection import ConnectionBase 40 | from ansible.utils.vars import merge_hash 41 | 42 | try: 43 | from ansible.module_utils._text import to_bytes, to_native 44 | except ImportError: 45 | from ansible.utils.unicode import to_bytes 46 | from ansible.utils.unicode import to_str as to_native 47 | 48 | try: 49 | from __main__ import display 50 | except ImportError: 51 | from ansible.utils.display import Display 52 | display = Display() 53 | 54 | 55 | class MachineCtl(object): 56 | 57 | # Suppress some diagnostic info that is not relevant when running 58 | # non-interactively. This is one notch below the default 'info' level; see 59 | # `man 1 systemd'. 60 | SYSTEMD_LOG_LEVEL = 'notice' 61 | 62 | # Prior to version 230, `machinectl' consumed all flags in the `shell' 63 | # invocation, including those intended for the executed command. See: 64 | # https://github.com/systemd/systemd/issues/2420 65 | MACHINECTL_GETOPT_FIX_VERSION = '230' 66 | 67 | def __init__(self, command=None): 68 | if command is not None: 69 | self.command = command 70 | else: 71 | self.command = distutils.spawn.find_executable('machinectl') 72 | if not self.command: 73 | raise AnsibleError('machinectl executable not found in PATH') 74 | 75 | self.version = self._version() 76 | 77 | @classmethod 78 | def machinectl_env(cls, **kwargs): 79 | ''' 80 | Copy the current environment, merging keyword arguments and setting 81 | the systemd log level. 82 | ''' 83 | 84 | return dict(merge_hash(os.environ, kwargs), SYSTEMD_LOG_LEVEL=cls.SYSTEMD_LOG_LEVEL) 85 | 86 | def _version(self): 87 | ''' Queries the installed version of machinectl/systemd ''' 88 | 89 | try: 90 | version_output = subprocess.check_output([self.command, '--version']) 91 | matched = re.match(r'\Asystemd\s+(\d+)\D', to_native(version_output)) 92 | return (matched.groups())[0] 93 | except subprocess.CalledProcessError as e: 94 | raise AnsibleError('failed to retrieve machinectl version: {0}'.format(e.message)) 95 | 96 | def property(self, wanted, machine=None): 97 | ''' Returns the value of a single machine property ''' 98 | for prop, value in self.show(machine, '--property={0}'.format(wanted)): 99 | if wanted == prop: 100 | return value 101 | 102 | def build_command(self, action, opts=[], args=[], machine=None): 103 | ''' 104 | Constructs a machinectl command with proper argument ordering. 105 | Special-cases arguments to the shell subcommand if appropriate. 106 | ''' 107 | 108 | local_cmd = [self.command] + opts + [action] 109 | 110 | if machine is not None: 111 | local_cmd.append(machine) 112 | 113 | if action == 'shell' and self.version < self.MACHINECTL_GETOPT_FIX_VERSION: 114 | local_cmd.append('--') 115 | 116 | return local_cmd + args 117 | 118 | def popen_command(self, action, opts=[], args=[], machine=None, **kwargs): 119 | ''' 120 | Opens a command targeting the the specified machine 121 | 122 | :arg action: byte string containing the machinectl subcommand 123 | :kwarg opts: a list of byte strings representing flags to machinectl 124 | :kwarg args: a list of byte string representing parameters specific to 125 | ``action`` 126 | :kwarg machine: a byte string representing a machine name 127 | :kwarg stdin: standard input of the opened process 128 | :type stdin: :data:`subprocess.PIPE`, file descriptor, or None 129 | :kwarg stdout: standard output of the opened process 130 | :type stdin: :data:`subprocess.PIPE`, file descriptor, or None 131 | :kwarg stderr: standard error of the opened process 132 | :type stdin: :data:`subprocess.PIPE`, :data:`subprocess.STDOUT`, file descriptor, or None 133 | :returns: an open process 134 | :rtype: :class:`subprocess.Popen` 135 | ''' 136 | 137 | machinectl_env = self.machinectl_env() 138 | local_cmd = self.build_command(action, opts=opts, args=args, machine=machine) 139 | 140 | display.vvv(u'EXEC {0}'.format(local_cmd,), host=(machine or 'NONE')) 141 | 142 | local_cmd = [to_bytes(i, errors='strict') for i in local_cmd] 143 | 144 | stdin = kwargs.get('stdin', None) 145 | stdout = kwargs.get('stdout', subprocess.PIPE) 146 | stderr = kwargs.get('stderr', subprocess.PIPE) 147 | 148 | # TODO why can't we set stdin to a pipe? 149 | return subprocess.Popen(local_cmd, env=machinectl_env, shell=False, 150 | stdin=stdin, stdout=stdout, stderr=stderr) 151 | 152 | def run_command(self, action, opts=[], args=[], machine=None, in_data=None): 153 | ''' 154 | Wrapper for :func:`popen_command` that handles passing input data to 155 | the opened process. 156 | 157 | Unlike :func:`popen_command`, does not accept arguments for standard 158 | input, standard output, or standard error, but does recognize the 159 | additional argument ``in_data``. 160 | 161 | :kwarg in_data: 162 | ''' 163 | 164 | p = self.popen_command(action, opts=opts, args=args, machine=machine) 165 | stdout, stderr = p.communicate(in_data) 166 | return (p.returncode, stdout, stderr) 167 | 168 | def list(self): 169 | ''' Returns a list of machine names ''' 170 | returncode, stdout, stderr = self.run_command('list', opts=['--no-legend']) 171 | 172 | for i in to_native(stdout.strip()).splitlines(): 173 | yield re.split(r'\s+', i) 174 | 175 | def show(self, machine=None, *args): 176 | ''' Yields machine properties in key-value pairs ''' 177 | returncode, stdout, stderr = self.run_command('show', machine=machine) 178 | 179 | for line in to_native(stdout).splitlines(): 180 | yield line.strip().split('=', 2) 181 | 182 | 183 | class Connection(ConnectionBase): 184 | ''' Local connection based on systemd's machinectl ''' 185 | 186 | transport = 'machinectl' 187 | 188 | # machinectl's shell subcommand expects to be connected to a terminal; 189 | # otherwise, it ignore standard input. This means that we can't use 190 | # pipelining -- quoting the SSH connection plugin: 191 | # 192 | # we can only use tty when we are not pipelining the modules. piping 193 | # data into /usr/bin/python inside a tty automatically invokes the 194 | # python interactive-mode but the modules are not compatible with the 195 | # interactive-mode ("unexpected indent" mainly because of empty lines) 196 | has_pipelining = False 197 | 198 | def __init__(self, play_context, new_stdin, *args, **kwargs): 199 | super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) 200 | 201 | if os.geteuid() != 0: 202 | raise AnsibleError('machinectl connection requires running as root') 203 | 204 | self.machinectl = MachineCtl(kwargs.get('machinectl_command')) 205 | self.remote_uid = None 206 | self.remote_gid = None 207 | self._flags = collections.defaultdict(lambda: False) 208 | 209 | def _parse_passwd(self, entry): 210 | if entry is None: 211 | return entry 212 | return entry.split(':') 213 | 214 | def _remote_passwd(self, user, passwd_path=None): 215 | if user is None: 216 | user = self._play_context.remote_user 217 | 218 | if user is None: 219 | return 220 | 221 | for getent in ['/bin/getent', '/usr/bin/getent']: 222 | try: 223 | returncode, stdout, stderr = self._run_command('shell', args=[getent, 'passwd', user]) 224 | except AnsibleError: 225 | pass 226 | 227 | if returncode == 0: 228 | return self._parse_passwd(stdout) 229 | 230 | try: 231 | if passwd_path is None: 232 | passwd_path = os.path.join(self.chroot, 'etc/passwd') 233 | 234 | with open(passwd_path, 'r') as passwdf: 235 | for entry in passwdf.readlines(): 236 | parsed = self._parse_passwd(entry) 237 | if parsed[0] == self._play_context.remote_user: 238 | return parsed 239 | except IOError: 240 | return 241 | 242 | def _connect(self): 243 | ''' Connection ain't real ''' 244 | super(Connection, self)._connect() 245 | 246 | if not self._connected: 247 | self.machine = self._play_context.remote_addr 248 | 249 | display.vvv(u'ESTABLISH MACHINECTL CONNECTION FOR USER: {0}'.format( 250 | self._play_context.remote_user or '?'), host=self.machine 251 | ) 252 | 253 | if self.machinectl.property('State', self.machine) != 'running': 254 | raise AnsibleError('machine {0} is not running'.format(self.machine)) 255 | 256 | self.chroot = self.machinectl.property('RootDirectory', self.machine) 257 | 258 | display.vvv(u'MACHINE RUNNING FROM HOST DIRECTORY {0}'.format(self.chroot), host=self.machine) 259 | 260 | if self._play_context.remote_user is not None: 261 | self.chown_files = True 262 | 263 | remote_passwd = self._remote_passwd(self._play_context.remote_user) 264 | if remote_passwd is not None: 265 | self.remote_uid = int(remote_passwd[2]) 266 | self.remote_gid = int(remote_passwd[3] or -1) 267 | else: 268 | raise AnsibleError('failed to find UID or GID for {0}'.format(self._play_context.remote_user)) 269 | else: 270 | self.chown_files = False 271 | 272 | self._connected = True 273 | 274 | def close(self): 275 | ''' Again, connection ain't real ''' 276 | super(Connection, self).close() 277 | self._connected = False 278 | 279 | def _prefix_login_path(self, remote_path): 280 | ''' Make sure that we put files into a standard path 281 | 282 | If a path is relative, then we need to choose where to put it. 283 | ssh chooses $HOME but we aren't guaranteed that a home dir will 284 | exist in any given chroot. So for now we're choosing "/" instead. 285 | This also happens to be the former default. 286 | 287 | Can revisit using $HOME instead if it's a problem 288 | ''' 289 | if not remote_path.startswith(os.path.sep): 290 | remote_path = os.path.join(os.path.sep, remote_path) 291 | 292 | return os.path.normpath(remote_path) 293 | 294 | def _run_command(self, action, opts=[], args=[], machine=None, in_data=None): 295 | p = self.machinectl.popen_command(action, opts=opts, args=args, machine=machine) 296 | 297 | stdout, stderr = p.communicate(in_data) 298 | 299 | return (p.returncode, stdout, stderr) 300 | 301 | def _examine_output(self, source, state, chunk, sudoable): 302 | ''' 303 | Takes a string, extracts complete lines from it, tests to see if they 304 | are a prompt, error message, etc., and sets appropriate flags in self. 305 | Prompt and success lines are removed. 306 | 307 | Returns the processed (i.e. possibly-edited) output and the unprocessed 308 | remainder (to be processed with the next chunk) as strings. 309 | ''' 310 | 311 | def diag_state(header, source, state, line): 312 | display.debug("{0}: (source={1}, state={2}): '{3}'".format(header, source, state, line.rstrip('\n'))) 313 | 314 | output = [] 315 | for l in chunk.splitlines(True): 316 | suppress_output = False 317 | 318 | if self._play_context.prompt and self.check_password_prompt(l): 319 | diag_state('become_prompt', source, state, l) 320 | self._flags['become_prompt'] = True 321 | suppress_output = True 322 | elif self._play_context.success_key and self.check_become_success(l): 323 | diag_state('become_success', source, state, l) 324 | self._flags['become_success'] = True 325 | suppress_output = True 326 | elif sudoable and self.check_incorrect_password(l): 327 | diag_state('become_error', source, state, l) 328 | self._flags['become_error'] = True 329 | elif sudoable and self.check_missing_password(l): 330 | diag_state('become_nopasswd_error', source, state, l) 331 | self._flags['become_nopasswd_error'] = True 332 | 333 | if not suppress_output: 334 | output.append(l) 335 | 336 | # The chunk we read was most likely a series of complete lines, but just 337 | # in case the last line was incomplete (and not a prompt, which we would 338 | # have removed from the output), we retain it to be processed with the 339 | # next chunk. 340 | 341 | remainder = '' 342 | if output and not output[-1].endswith('\n'): 343 | remainder = output[-1] 344 | output = output[:-1] 345 | 346 | return ''.join(output), remainder 347 | 348 | # Used by _run() to kill processes on failures 349 | @staticmethod 350 | def _terminate_process(p): 351 | """ Terminate a process, ignoring errors """ 352 | try: 353 | p.terminate() 354 | except (OSError, IOError): 355 | pass 356 | 357 | def exec_command(self, cmd, in_data=None, sudoable=False): 358 | super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) 359 | 360 | if in_data is not None: 361 | raise AnsibleError('the machinectl connection cannot perform pipelining') 362 | 363 | opts = [] 364 | # --uid only recognized with `shell' subcommand 365 | if self.remote_uid is not None: 366 | display.vvv(u'RUN AS {0} (UID {1})'.format(self._play_context.remote_user, self.remote_uid)) 367 | opts = ['--uid={0}'.format(self.remote_uid)] 368 | 369 | master, slave = pty.openpty() 370 | p = self.machinectl.popen_command('shell', opts=opts, args=shlex.split(cmd), 371 | machine=self.machine, stdin=slave) 372 | 373 | os.close(slave) 374 | stdin = os.fdopen(master, 'wb', 0) 375 | 376 | ## SSH state machine 377 | # 378 | # Now we read and accumulate output from the running process until it 379 | # exits. Depending on the circumstances, we may also need to write an 380 | # escalation password and/or pipelined input to the process. 381 | 382 | states = [ 383 | 'awaiting_prompt', 'awaiting_escalation', 'ready_to_send', 'awaiting_exit' 384 | ] 385 | 386 | # Are we requesting privilege escalation? Right now, we may be invoked 387 | # to execute sftp/scp with sudoable=True, but we can request escalation 388 | # only when using ssh. Otherwise we can send initial data straightaway. 389 | 390 | state = states.index('ready_to_send') 391 | if self._play_context.prompt: 392 | # We're requesting escalation with a password, so we have to 393 | # wait for a password prompt. 394 | state = states.index('awaiting_prompt') 395 | display.debug('Initial state: %s: %s' % (states[state], self._play_context.prompt)) 396 | elif self._play_context.become and self._play_context.success_key: 397 | # We're requesting escalation without a password, so we have to 398 | # detect success/failure before sending any initial data. 399 | state = states.index('awaiting_escalation') 400 | display.debug('Initial state: %s: %s' % (states[state], self._play_context.success_key)) 401 | 402 | # We store accumulated stdout and stderr output from the process here, 403 | # but strip any privilege escalation prompt/confirmation lines first. 404 | # Output is accumulated into tmp_*, complete lines are extracted into 405 | # an array, then checked and removed or copied to stdout or stderr. We 406 | # set any flags based on examining the output in self._flags. 407 | 408 | stdout = stderr = '' 409 | tmp_stdout = tmp_stderr = '' 410 | 411 | self._flags = dict( 412 | become_prompt=False, become_success=False, 413 | become_error=False, become_nopasswd_error=False 414 | ) 415 | 416 | # select timeout should be longer than the connect timeout, otherwise 417 | # they will race each other when we can't connect, and the connect 418 | # timeout usually fails 419 | timeout = 2 + self._play_context.timeout 420 | rpipes = [p.stdout, p.stderr] 421 | for fd in rpipes: 422 | fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK) 423 | 424 | # If we can send initial data without waiting for anything, we do so 425 | # before we call select. 426 | 427 | if states[state] == 'ready_to_send' and in_data: 428 | # TODO 429 | #self._send_initial_data(stdin, in_data) 430 | state += 1 431 | 432 | while True: 433 | rfd, wfd, efd = select.select(rpipes, [], [], timeout) 434 | 435 | # We pay attention to timeouts only while negotiating a prompt. 436 | 437 | if not rfd: 438 | if state <= states.index('awaiting_escalation'): 439 | # If the process has already exited, then it's not really a 440 | # timeout; we'll let the normal error handling deal with it. 441 | if p.poll() is not None: 442 | break 443 | self._terminate_process(p) 444 | raise AnsibleError('Timeout (%ds) waiting for privilege escalation prompt: %s' % (timeout, stdout)) 445 | 446 | # Read whatever output is available on stdout and stderr, and stop 447 | # listening to the pipe if it's been closed. 448 | 449 | if p.stdout in rfd: 450 | chunk = to_native(p.stdout.read()) 451 | if chunk == '': 452 | rpipes.remove(p.stdout) 453 | tmp_stdout += chunk 454 | display.debug("stdout chunk (state=%s):\n>>>%s<<<\n" % (state, chunk)) 455 | 456 | if p.stderr in rfd: 457 | chunk = to_native(p.stderr.read()) 458 | if chunk == '': 459 | rpipes.remove(p.stderr) 460 | tmp_stderr += chunk 461 | display.debug("stderr chunk (state=%s):\n>>>%s<<<\n" % (state, chunk)) 462 | 463 | # We examine the output line-by-line until we have negotiated any 464 | # privilege escalation prompt and subsequent success/error message. 465 | # Afterwards, we can accumulate output without looking at it. 466 | 467 | if state < states.index('ready_to_send'): 468 | if tmp_stdout: 469 | output, unprocessed = self._examine_output('stdout', states[state], tmp_stdout, sudoable) 470 | stdout += output 471 | tmp_stdout = unprocessed 472 | 473 | if tmp_stderr: 474 | output, unprocessed = self._examine_output('stderr', states[state], tmp_stderr, sudoable) 475 | stderr += output 476 | tmp_stderr = unprocessed 477 | else: 478 | stdout += tmp_stdout 479 | stderr += tmp_stderr 480 | tmp_stdout = tmp_stderr = '' 481 | 482 | # If we see a privilege escalation prompt, we send the password. 483 | # (If we're expecting a prompt but the escalation succeeds, we 484 | # didn't need the password and can carry on regardless.) 485 | 486 | if states[state] == 'awaiting_prompt': 487 | if self._flags['become_prompt']: 488 | display.debug('Sending become_pass in response to prompt') 489 | stdin.write('{0}\n'.format(to_bytes(self._play_context.become_pass ))) 490 | self._flags['become_prompt'] = False 491 | state += 1 492 | elif self._flags['become_success']: 493 | state += 1 494 | 495 | # We've requested escalation (with or without a password), now we 496 | # wait for an error message or a successful escalation. 497 | 498 | if states[state] == 'awaiting_escalation': 499 | if self._flags['become_success']: 500 | display.debug('Escalation succeeded') 501 | self._flags['become_success'] = False 502 | state += 1 503 | elif self._flags['become_error']: 504 | display.debug('Escalation failed') 505 | self._terminate_process(p) 506 | self._flags['become_error'] = False 507 | raise AnsibleError('Incorrect %s password' % self._play_context.become_method) 508 | elif self._flags['become_nopasswd_error']: 509 | display.debug('Escalation requires password') 510 | self._terminate_process(p) 511 | self._flags['become_nopasswd_error'] = False 512 | raise AnsibleError('Missing %s password' % self._play_context.become_method) 513 | elif self._flags['become_prompt']: 514 | # This shouldn't happen, because we should see the "Sorry, 515 | # try again" message first. 516 | display.debug('Escalation prompt repeated') 517 | self._terminate_process(p) 518 | self._flags['become_prompt'] = False 519 | raise AnsibleError('Incorrect %s password' % self._play_context.become_method) 520 | 521 | # Once we're sure that the privilege escalation prompt, if any, has 522 | # been dealt with, we can send any initial data and start waiting 523 | # for output. 524 | 525 | if states[state] == 'ready_to_send': 526 | if in_data: 527 | self._send_initial_data(stdin, in_data) 528 | state += 1 529 | 530 | # Now we're awaiting_exit: has the child process exited? If it has, 531 | # and we've read all available output from it, we're done. 532 | 533 | if p.poll() is not None: 534 | if not rpipes or not rfd: 535 | break 536 | 537 | # When ssh has ControlMaster (+ControlPath/Persist) enabled, the 538 | # first connection goes into the background and we never see EOF 539 | # on stderr. If we see EOF on stdout and the process has exited, 540 | # we're probably done. We call select again with a zero timeout, 541 | # just to make certain we don't miss anything that may have been 542 | # written to stderr between the time we called select() and when 543 | # we learned that the process had finished. 544 | 545 | if p.stdout not in rpipes: 546 | timeout = 0 547 | continue 548 | 549 | # If the process has not yet exited, but we've already read EOF from 550 | # its stdout and stderr (and thus removed both from rpipes), we can 551 | # just wait for it to exit. 552 | 553 | elif not rpipes: 554 | p.wait() 555 | break 556 | 557 | # Otherwise there may still be outstanding data to read. 558 | 559 | return (p.returncode, stdout, stderr) 560 | 561 | def put_file(self, in_path, out_path): 562 | super(Connection, self).put_file(in_path, out_path) 563 | display.vvv(u'PUT {0} TO {1}'.format(in_path, out_path), host=self.machine) 564 | 565 | # Set file permissions prior to transfer so that they will be correct 566 | # on the container 567 | try: 568 | if self.remote_uid is not None: 569 | os.chown(in_path, self.remote_uid, self.remote_gid or -1) 570 | except OSError: 571 | raise AnsibleError('failed to change ownership on file {0} to user {1}'.format(in_path, self._play_context.remote_user)) 572 | 573 | out_path = self._prefix_login_path(out_path) 574 | if not os.path.exists(to_bytes(in_path, errors='strict')): 575 | raise AnsibleFileNotFound('file or module does not exist: {0}'.format(in_path)) 576 | 577 | # Okay, this is definitely not a great idea to do, that's pretty ugly, 578 | # but we have no choice... Let me explain 579 | # You cannot "copy-to --force" with machinectl. There is a request to 580 | # do that, but unaddressed as of today: 581 | # https://github.com/systemd/systemd/issues/9441 582 | # So you cannot overwrite an existing file. This is very annoying when 583 | # pushing DIRECTORIES... as the same ansible file (with the same name 584 | # on the remote target) must be overwritten for each file in the 585 | # directory. 586 | # Without removing the file first, we get an error: "file exists". 587 | remove_cmd = self._shell.remove(out_path, recurse=True) 588 | remove_sh_cmd = [self._play_context.executable, '-c', remove_cmd] 589 | returncode, stdout, stderr = self._run_command('shell', args=remove_sh_cmd, machine=self.machine) 590 | if returncode != 0: 591 | raise AnsibleError('failed to perform cleanup of file {0}:\n{1}\n{2}'.format(out_path, stdout, stderr)) 592 | returncode, stdout, stderr = self._run_command('copy-to', args=[in_path, out_path], machine=self.machine) 593 | if returncode != 0: 594 | raise AnsibleError('failed to transfer file {0} to {1}:\n{2}\n{3}'.format(in_path, out_path, stdout, stderr)) 595 | 596 | def fetch_file(self, in_path, out_path): 597 | super(Connection, self).fetch_file(in_path, out_path) 598 | display.vvv(u'FETCH {0} TO {1}'.format(in_path, out_path), host=self.machine) 599 | 600 | in_path = self._prefix_login_path(in_path) 601 | 602 | returncode, stdout, stderr = self._run_command('copy-from', args=[in_path, out_path], machine=self.machine) 603 | 604 | if returncode != 0: 605 | raise AnsibleError('failed to transfer file {0} from {1}:\n{2}\n{3}'.format(out_path, in_path, stdout, stderr)) 606 | 607 | # TODO might not be necessary? 608 | # Reset file permissions to current user after transferring from 609 | # container 610 | try: 611 | if self.remote_uid is not None: 612 | os.chown(out_path, os.geteuid(), os.getegid() or -1) 613 | except OSError: 614 | raise AnsibleError('failed to change ownership on file {0} to user {1}'.format(out_path, os.getlogin())) 615 | 616 | -------------------------------------------------------------------------------- /contrib/inventory/machinectl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Dynamic inventory for machinectl virtual machines and containers 4 | # (c) 2016, Matt Schreiber 5 | # 6 | # This machinectl dynamic inventory is distributed in the hope that it will be 7 | # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 9 | # Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with Ansible. If not, see . 13 | 14 | from connection_plugins.machinectl import MachineCtl 15 | from connection_plugins.machinectl import Connection as MachineCtlConnection 16 | 17 | import sys 18 | import json 19 | 20 | machinectl = MachineCtl() 21 | 22 | result = {} 23 | result['all'] = {} 24 | result['all']['hosts'] = [m[0] for m in machinectl.list()] 25 | result['all']['vars'] = {'machined_config': dict(machinectl.show())} 26 | result['all']['vars']['ansible_connection'] = MachineCtlConnection.transport 27 | result['_meta'] = {'hostvars': {mn: {'machine_config': dict(machinectl.show(mn))} for mn in result['all']['hosts']}} 28 | 29 | 30 | if len(sys.argv) == 2 and sys.argv[1] == '--list': 31 | print(json.dumps(result)) 32 | elif len(sys.argv) == 3 and sys.argv[1] == '--host': 33 | print(json.dumps(result['_meta']['hostvars'].get(sys.argv[2], {}))) 34 | else: 35 | print("Need an argument, either --list or --host ") 36 | -------------------------------------------------------------------------------- /dobi.yaml: -------------------------------------------------------------------------------- 1 | meta: 2 | project: ansible-connection-machinectl 3 | default: all 4 | 5 | env=fedora-release: 6 | variables: 7 | - FEDORA_RELEASE=29 8 | - HUB_USERNAME=tomeon 9 | 10 | image=base: 11 | image: '{env.HUB_USERNAME}/fedora-mkosi' 12 | context: 'docker' 13 | dockerfile: 'Dockerfile.base' 14 | pull-base-image-on-build: true 15 | args: &BASE_ARGS 16 | FEDORA_RELEASE: '{env.FEDORA_RELEASE:29}' 17 | depends: 18 | - 'fedora-release' 19 | tags: 20 | - '{env.FEDORA_RELEASE}' 21 | 22 | image=master: 23 | image: '{env.HUB_USERNAME}/fedora-mkosi' 24 | context: 'docker' 25 | dockerfile: 'Dockerfile.master' 26 | tags: 27 | - '{env.FEDORA_RELEASE}-master' 28 | depends: 29 | - 'base' 30 | - 'fedora-release' 31 | args: 32 | <<: *BASE_ARGS 33 | 34 | image=tox: 35 | image: '{env.HUB_USERNAME}/fedora-mkosi' 36 | context: 'docker' 37 | dockerfile: 'Dockerfile.tox' 38 | tags: 39 | - '{env.FEDORA_RELEASE}-tox' 40 | depends: 41 | - 'master' 42 | - 'fedora-release' 43 | args: 44 | <<: *BASE_ARGS 45 | 46 | image=travis: 47 | image: '{env.HUB_USERNAME}/fedora-mkosi' 48 | context: 'docker' 49 | dockerfile: 'Dockerfile.travis' 50 | tags: 51 | - '{env.FEDORA_RELEASE}-travis' 52 | depends: 53 | - 'tox' 54 | - 'fedora-release' 55 | args: 56 | <<: *BASE_ARGS 57 | 58 | image=vagrant: 59 | image: '{env.HUB_USERNAME}/fedora-mkosi' 60 | context: 'docker' 61 | dockerfile: 'Dockerfile.vagrant' 62 | tags: 63 | - '{env.FEDORA_RELEASE}-vagrant' 64 | depends: 65 | - 'tox' 66 | - 'fedora-release' 67 | args: 68 | <<: *BASE_ARGS 69 | 70 | alias=push: 71 | tasks: 72 | - 'base:push' 73 | - 'master:push' 74 | - 'tox:push' 75 | - 'travis:push' 76 | - 'vagrant:push' 77 | 78 | alias=all: 79 | tasks: 80 | - 'base' 81 | - 'master' 82 | - 'tox' 83 | - 'travis' 84 | - 'vagrant' 85 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Dockerfile* 3 | !files/ 4 | -------------------------------------------------------------------------------- /docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | ARG FEDORA_RELEASE=29 2 | 3 | FROM fedora:${FEDORA_RELEASE} 4 | 5 | ENV container docker 6 | 7 | RUN dnf -y update \ 8 | && dnf -y install \ 9 | # Build nspawn containers 10 | mkosi \ 11 | # mkosi optional deps not specified as `Recommends` in mkosi.spec 12 | # N.B. psmisc contains fuser 13 | e2fsprogs psmisc xfsprogs zypper \ 14 | # https://github.com/42BV/docker-mkosi/blob/master/Dockerfile 15 | util-linux-user git \ 16 | # Runs tests against multiple pythons 17 | tox \ 18 | # Provides `machinectl` 19 | systemd-container \ 20 | # Needed for getting a TTY in an nspawn container 21 | polkit \ 22 | # Arch containers need entropy when setting up Pacman keyring 23 | haveged \ 24 | # For resizing the /var/lib/machines.raw disk image 25 | qemu-img \ 26 | && dnf -y clean all \ 27 | && systemctl enable haveged.service 28 | 29 | VOLUME ["/sys/fs/cgroup"] 30 | 31 | CMD ["/usr/sbin/init"] 32 | -------------------------------------------------------------------------------- /docker/Dockerfile.master: -------------------------------------------------------------------------------- 1 | ARG FEDORA_RELEASE=29 2 | 3 | FROM tomeon/fedora-mkosi:${FEDORA_RELEASE} 4 | 5 | # We use the `-a` option to mkosi, which is only in master at the moment 6 | ADD https://raw.githubusercontent.com/systemd/mkosi/master/mkosi /usr/local/bin/mkosi 7 | 8 | RUN chmod 0755 /usr/local/bin/mkosi 9 | -------------------------------------------------------------------------------- /docker/Dockerfile.tox: -------------------------------------------------------------------------------- 1 | ARG FEDORA_RELEASE=29 2 | 3 | FROM tomeon/fedora-mkosi:${FEDORA_RELEASE}-master 4 | 5 | RUN dnf -y update \ 6 | # Runs tests against multiple pythons 7 | && dnf -y install tox \ 8 | && dnf -y clean all 9 | 10 | # Dummy fuser that does nothing but echo back its arguments 11 | # TODO why did I add this :/ 12 | #COPY files/fuser.dummy /usr/local/bin/fuser 13 | -------------------------------------------------------------------------------- /docker/Dockerfile.travis: -------------------------------------------------------------------------------- 1 | ARG FEDORA_RELEASE=29 2 | 3 | FROM tomeon/fedora-mkosi:${FEDORA_RELEASE}-tox 4 | 5 | RUN dnf -y update \ 6 | && dnf -y install 'dnf-command(copr)' \ 7 | && dnf -y copr enable tomeon/python-tox-plugins \ 8 | && dnf -y install parallel python3-tox-travis \ 9 | && dnf -y autoremove 'dnf-command(copr)' \ 10 | && dnf -y clean all 11 | -------------------------------------------------------------------------------- /docker/Dockerfile.vagrant: -------------------------------------------------------------------------------- 1 | ARG FEDORA_RELEASE=29 2 | 3 | FROM tomeon/fedora-mkosi:${FEDORA_RELEASE}-tox 4 | 5 | RUN useradd -U -m -s /bin/bash vagrant \ 6 | && install -dm 0700 --owner vagrant --group vagrant /vagrant \ 7 | && dnf -y install sudo parallel \ 8 | && dnf -y clean all 9 | 10 | COPY files/vagrant.sudoers.d /etc/sudoers.d/vagrant 11 | 12 | # Causes the systemd-tempfiles unit to create a link at /run/shm pointing to 13 | # /dev/shm 14 | COPY files/mkosi.tmpfiles.d.conf /etc/tmpfiles.d/mkosi.conf 15 | 16 | RUN chmod 440 /etc/sudoers.d/vagrant && visudo -c 17 | 18 | ADD https://raw.githubusercontent.com/hashicorp/vagrant/master/keys/vagrant.pub /tmp/vagrant.pub 19 | 20 | RUN dnf install -y openssh-server \ 21 | && install -dm0700 --owner vagrant --group vagrant ~vagrant/.ssh \ 22 | && install -Dm0600 --owner vagrant --group vagrant /tmp/vagrant.pub ~vagrant/.ssh/authorized_keys \ 23 | && rm /tmp/vagrant.pub \ 24 | && dnf -y clean all \ 25 | && systemctl enable sshd.service 26 | -------------------------------------------------------------------------------- /docker/files/docker.default: -------------------------------------------------------------------------------- 1 | DOCKER_OPTS='-H tcp://' 2 | -------------------------------------------------------------------------------- /docker/files/fuser.dummy: -------------------------------------------------------------------------------- 1 | #!/bin/echo # 2 | -------------------------------------------------------------------------------- /docker/files/mkosi.tmpfiles.d.conf: -------------------------------------------------------------------------------- 1 | # Create link at /run/shm pointing to /dev/shm 2 | L /run/shm - - - - /dev/shm 3 | -------------------------------------------------------------------------------- /docker/files/vagrant.sudoers.d: -------------------------------------------------------------------------------- 1 | Defaults:vagrant !requiretty,!tty_tickets 2 | Defaults:vagrant env_keep += "SSH_AGENT_PID SSH_AUTH_SOCK" 3 | 4 | vagrant ALL=(ALL:ALL) NOPASSWD: ALL 5 | -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | 2 | [arch] 3 | 4 | [local] 5 | localhost 6 | 7 | [test] 8 | travis 9 | 10 | [arch:children] 11 | local 12 | 13 | [arch:vars] 14 | ansible_python_interpreter = /usr/bin/python2 15 | 16 | [local:vars] 17 | ansible_connection = local 18 | 19 | [test:vars] 20 | ansible_host = 127.0.0.1 21 | ansible_connection = local 22 | 23 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: your name 3 | description: your description 4 | company: your company (optional) 5 | 6 | # If the issue tracker for your role is not on github, uncomment the 7 | # next line and provide a value 8 | # issue_tracker_url: http://example.com/issue/tracker 9 | 10 | # Some suggested licenses: 11 | # - BSD (default) 12 | # - MIT 13 | # - GPLv2 14 | # - GPLv3 15 | # - Apache 16 | # - CC-BY 17 | license: license (GPLv2, CC-BY, etc) 18 | 19 | min_ansible_version: 1.2 20 | 21 | # Optionally specify the branch Galaxy will use when accessing the GitHub 22 | # repo for this role. During role install, if no tags are available, 23 | # Galaxy will use this branch. During import Galaxy will access files on 24 | # this branch. If travis integration is cofigured, only notification for this 25 | # branch will be accepted. Otherwise, in all cases, the repo's default branch 26 | # (usually master) will be used. 27 | #github_branch: 28 | 29 | # 30 | # Below are all platforms currently available. Just uncomment 31 | # the ones that apply to your role. If you don't see your 32 | # platform on this list, let us know and we'll get it added! 33 | # 34 | #platforms: 35 | #- name: EL 36 | # versions: 37 | # - all 38 | # - 5 39 | # - 6 40 | # - 7 41 | #- name: GenericUNIX 42 | # versions: 43 | # - all 44 | # - any 45 | #- name: OpenBSD 46 | # versions: 47 | # - all 48 | # - 5.6 49 | # - 5.7 50 | # - 5.8 51 | # - 5.9 52 | # - 6.0 53 | #- name: Fedora 54 | # versions: 55 | # - all 56 | # - 16 57 | # - 17 58 | # - 18 59 | # - 19 60 | # - 20 61 | # - 21 62 | # - 22 63 | # - 23 64 | #- name: opensuse 65 | # versions: 66 | # - all 67 | # - 12.1 68 | # - 12.2 69 | # - 12.3 70 | # - 13.1 71 | # - 13.2 72 | #- name: MacOSX 73 | # versions: 74 | # - all 75 | # - 10.10 76 | # - 10.11 77 | # - 10.12 78 | # - 10.7 79 | # - 10.8 80 | # - 10.9 81 | #- name: IOS 82 | # versions: 83 | # - all 84 | # - any 85 | #- name: Solaris 86 | # versions: 87 | # - all 88 | # - 10 89 | # - 11.0 90 | # - 11.1 91 | # - 11.2 92 | # - 11.3 93 | #- name: SmartOS 94 | # versions: 95 | # - all 96 | # - any 97 | #- name: eos 98 | # versions: 99 | # - all 100 | # - Any 101 | #- name: Windows 102 | # versions: 103 | # - all 104 | # - 2012R2 105 | #- name: Amazon 106 | # versions: 107 | # - all 108 | # - 2013.03 109 | # - 2013.09 110 | #- name: GenericBSD 111 | # versions: 112 | # - all 113 | # - any 114 | #- name: Junos 115 | # versions: 116 | # - all 117 | # - any 118 | #- name: FreeBSD 119 | # versions: 120 | # - all 121 | # - 10.0 122 | # - 10.1 123 | # - 10.2 124 | # - 10.3 125 | # - 8.0 126 | # - 8.1 127 | # - 8.2 128 | # - 8.3 129 | # - 8.4 130 | # - 9.0 131 | # - 9.1 132 | # - 9.1 133 | # - 9.2 134 | # - 9.3 135 | #- name: Ubuntu 136 | # versions: 137 | # - all 138 | # - lucid 139 | # - maverick 140 | # - natty 141 | # - oneiric 142 | # - precise 143 | # - quantal 144 | # - raring 145 | # - saucy 146 | # - trusty 147 | # - utopic 148 | # - vivid 149 | # - wily 150 | # - xenial 151 | #- name: SLES 152 | # versions: 153 | # - all 154 | # - 10SP3 155 | # - 10SP4 156 | # - 11 157 | # - 11SP1 158 | # - 11SP2 159 | # - 11SP3 160 | # - 11SP4 161 | # - 12 162 | # - 12SP1 163 | #- name: GenericLinux 164 | # versions: 165 | # - all 166 | # - any 167 | #- name: NXOS 168 | # versions: 169 | # - all 170 | # - any 171 | #- name: Debian 172 | # versions: 173 | # - all 174 | # - etch 175 | # - jessie 176 | # - lenny 177 | # - sid 178 | # - squeeze 179 | # - stretch 180 | # - wheezy 181 | 182 | galaxy_tags: [] 183 | # List tags for your role here, one per line. A tag is 184 | # a keyword that describes and categorizes the role. 185 | # Users find roles by searching for tags. Be sure to 186 | # remove the '[]' above if you add tags to this list. 187 | # 188 | # NOTE: A tag is limited to a single word comprised of 189 | # alphanumeric characters. Maximum 20 tags per role. 190 | 191 | dependencies: [] 192 | # List your role dependencies here, one per line. 193 | # Be sure to remove the '[]' above if you add dependencies 194 | # to this list. -------------------------------------------------------------------------------- /mkosi/mkosi.default: -------------------------------------------------------------------------------- 1 | [Output] 2 | Format = tar 3 | Bootable = no 4 | -------------------------------------------------------------------------------- /mkosi/mkosi.files/mkosi.arch: -------------------------------------------------------------------------------- 1 | [Distribution] 2 | Distribution = arch 3 | 4 | [Output] 5 | Output = arch.tar.xz 6 | OutputDirectory = mkosi.output/arch 7 | Format = tar 8 | Bootable = no 9 | 10 | [Partitions] 11 | RootSize = 1500M 12 | 13 | [Packages] 14 | Packages = python3 python2 15 | 16 | [Validation] 17 | CheckSum = yes 18 | -------------------------------------------------------------------------------- /mkosi/mkosi.files/mkosi.bionic: -------------------------------------------------------------------------------- 1 | [Distribution] 2 | Distribution = ubuntu 3 | Release = bionic 4 | 5 | [Output] 6 | Output = bionic.tar.xz 7 | OutputDirectory = mkosi.output/bionic 8 | Format = tar 9 | Bootable = no 10 | 11 | [Partitions] 12 | RootSize = 300M 13 | 14 | [Packages] 15 | Packages = python3 python dbus 16 | 17 | [Validation] 18 | CheckSum = yes 19 | -------------------------------------------------------------------------------- /mkosi/mkosi.files/mkosi.fedora: -------------------------------------------------------------------------------- 1 | [Distribution] 2 | Distribution = fedora 3 | Release = 29 4 | 5 | [Output] 6 | Output = fedora.tar.xz 7 | OutputDirectory = mkosi.output/fedora 8 | Format = tar 9 | Bootable = no 10 | 11 | [Packages] 12 | Packages = --setopt=tsflags=nodocs python3 python2 python systemd 13 | 14 | [Validation] 15 | CheckSum = yes 16 | -------------------------------------------------------------------------------- /mkosi/mkosi.files/mkosi.stretch: -------------------------------------------------------------------------------- 1 | [Distribution] 2 | Distribution = debian 3 | Release = stretch 4 | 5 | [Output] 6 | Output = stretch.tar.xz 7 | OutputDirectory = mkosi.output/stretch 8 | Format = tar 9 | Bootable = no 10 | 11 | [Partitions] 12 | RootSize = 300M 13 | 14 | [Packages] 15 | Packages = python3 python dbus 16 | 17 | [Validation] 18 | CheckSum = yes 19 | -------------------------------------------------------------------------------- /mkosi/mkosi.files/mkosi.xenial: -------------------------------------------------------------------------------- 1 | [Distribution] 2 | Distribution = ubuntu 3 | Release = xenial 4 | 5 | [Output] 6 | Output = xenial.tar.xz 7 | OutputDirectory = mkosi.output/xenial 8 | Format = tar 9 | Bootable = no 10 | 11 | [Partitions] 12 | RootSize = 300M 13 | 14 | [Packages] 15 | Packages = python3 python dbus 16 | 17 | [Validation] 18 | CheckSum = yes 19 | -------------------------------------------------------------------------------- /mkosi/mkosi.nspawn: -------------------------------------------------------------------------------- 1 | [Exec] 2 | # See https://github.com/systemd/systemd/issues/9563#issuecomment-415241310 3 | Parameters=systemd.legacy_systemd_cgroup_controller=yes 4 | -------------------------------------------------------------------------------- /scripts/build-images: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Tries to figure out where the mkosi build directory is, and, if the directory 4 | # could be found, executes mkosi to build all images 5 | 6 | set -euo pipefail 7 | 8 | looks_like_mkosi_dir() { 9 | [[ -d "${1:?whoopsie}" ]] && { [[ -f "${1}/mkosi.default" ]] || [[ -d "${1}/mkosi.files" ]] ; } 10 | } 11 | 12 | if [[ -z "${MKOSI_DIR:-}" ]]; then 13 | candidates=() 14 | toplevel='' 15 | 16 | if command -v git &>/dev/null && toplevel="$(git rev-parse --show-toplevel 2>/dev/null)"; then 17 | candidates+=("${toplevel}/mkosi") 18 | fi 19 | 20 | if toplevel="$(readlink -f "${0%/*}/../mkosi")"; then 21 | candidates+=("${toplevel}/mkosi") 22 | fi 23 | 24 | candidates+=(/vagrant/mkosi /mkosi "$(pwd -P)") 25 | 26 | for candidate in "${candidates[@]}"; do 27 | if looks_like_mkosi_dir "$candidate"; then 28 | MKOSI_DIR="$candidate" 29 | break 30 | fi 31 | done 32 | fi 33 | 34 | if [[ -z "${MKOSI_DIR:-}" ]]; then 35 | echo 1>&2 "Unable to find mkosi directory; please set MKOSI_DIR." 36 | exit 1 37 | fi 38 | 39 | cd "$MKOSI_DIR" || exit 40 | 41 | if (( $# < 1 )) && [[ -d "${MKOSI_DIR}/mkosi.files" ]]; then 42 | set -- -a 43 | fi 44 | 45 | exec mkosi "$@" 46 | -------------------------------------------------------------------------------- /scripts/configure-docker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Writes out /etc/docker/daemon.json and restarts the Docker service if the 4 | # daemon configuration has changed 5 | 6 | set -eu 7 | 8 | files_identical() { 9 | [ -e "${1:?whoopsie}" ] && [ -e "${2:?oh no}" ] \ 10 | && [ "$(md5sum "$1" | awk '{print $1}')" = "$(md5sum "$2" | awk '{print $1}')" ] 11 | } 12 | 13 | sd_booted() { 14 | [ -d /run/systemd/system ] && ! [ -L /run/systemd/system ] \ 15 | && { command -v systemctl && systemctl is-system-running ; } 1>/dev/null 2>&1 16 | } 17 | 18 | if [ $# -gt 0 ]; then 19 | tmpfile="$(mktemp --tmpdir= "${0##*/}.daemon.json.XXXXXXXXXXX")" 20 | echo "$1" > "$tmpfile" 21 | 22 | if ! [ -e /etc/docker/daemon.json ] || ! files_identical "$tmpfile" /etc/docker/daemon.json; then 23 | install -dm0700 --owner root --group root /etc/docker 24 | mv "$tmpfile" /etc/docker/daemon.json 25 | chmod 0600 /etc/docker/daemon.json 26 | 27 | if sd_booted; then 28 | systemctl restart docker 29 | elif command -v service 1>/dev/null 2>&1; then 30 | service docker restart 31 | fi 32 | 33 | fi 34 | fi 35 | 36 | if sd_booted; then 37 | if ! systemctl is-active docker 1>/dev/null; then 38 | systemctl start docker 39 | fi 40 | elif ! docker run hello-world 1>/dev/null 2>&1; then 41 | service docker start 42 | fi 43 | 44 | exec docker run hello-world 1>/dev/null 45 | -------------------------------------------------------------------------------- /scripts/docker-run-travis-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(pwd -P)}" 4 | TRAVIS_BUILD_DIR_BIND_DEST="${TRAVIS_BUILD_DIR_BIND_DEST:-/vagrant}" 5 | 6 | exec docker run \ 7 | --detach \ 8 | --rm \ 9 | --privileged \ 10 | --cap-add SYS_ADMIN \ 11 | -e ANSIBLE \ 12 | -e ANSIBLE_DEBUG \ 13 | -e CI \ 14 | -e TRAVIS \ 15 | -e TRAVIS_BRANCH \ 16 | -e TRAVIS_JOB_ID \ 17 | -v "${TRAVIS_BUILD_DIR}:${TRAVIS_BUILD_DIR_BIND_DEST}" \ 18 | -v /sys/fs/cgroup:/sys/fs/cgroup:ro \ 19 | --tmpfs /tmp:exec \ 20 | --tmpfs /run:exec \ 21 | "$@" 22 | -------------------------------------------------------------------------------- /scripts/download-images: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if (( $# < 1 )); then 6 | printf 1>&2 -- 'Usage: %s IMAGE1 [IMAGE2 ...]\n' "${BASH_SOURCE[0]##*/}" 7 | exit 1 8 | fi 9 | 10 | export IMAGE_BASEURL="${IMAGE_BASEURL:-https://baxterstockman.keybase.pub/osi}" 11 | 12 | in_parallel() { 13 | parallel --tag --line-buffer --halt now,fail=1 "$@" 14 | } 15 | 16 | _pull_tar() { 17 | image="${1?:ouch}" 18 | image="${image##*/}" 19 | machinectl pull-tar --verify=no --force "${IMAGE_BASEURL}/${image}/${image}.tar.xz" 20 | } 21 | 22 | pull_tar_parallel() { 23 | export -f _pull_tar 24 | in_parallel _pull_tar {} ::: "$@" 25 | } 26 | 27 | pull_tar_loop() { 28 | for image in "$@"; do 29 | _pull_tar "${image##*/}" 30 | done 31 | } 32 | 33 | _import_tar() { 34 | image="${1?:ouch}" 35 | image="${image##*/}" 36 | image_basename="${image##*/}.tar.xz" 37 | curl -fLO "${IMAGE_BASEURL}/${image##*/}/${image##*/}.tar.xz" 38 | machinectl import-tar --verify=no --force "$image_basename" 39 | } 40 | 41 | import_tar_parallel() { 42 | export -f _import_tar 43 | in_parallel _import_tar {} ::: "$@" 44 | } 45 | 46 | import_tar_loop() { 47 | for image in "$@"; do 48 | _import_tar "${image##*/}" 49 | done 50 | } 51 | 52 | if command -v parallel &>/dev/null; then 53 | pull_tar=pull_tar_parallel 54 | import_tar=import_tar_parallel 55 | else 56 | pull_tar=pull_tar_loop 57 | import_tar=import_tar_loop 58 | fi 59 | 60 | wanted_images=("${@##*/}") 61 | 62 | declare -A have_images=() 63 | while read -r image _; do 64 | have_images[$image]=1 65 | done < <(machinectl --no-pager --no-legend list-images) 66 | 67 | needed_images=() 68 | for image in "${wanted_images[@]}"; do 69 | if [[ "${have_images[$image]:-0}" != 1 ]]; then 70 | needed_images+=("$image") 71 | fi 72 | done 73 | 74 | set -- "${needed_images[@]}" 75 | 76 | if (( $# < 1 )); then 77 | printf 1>&2 -- 'Already have all images: %s\n' "${wanted_images[*]}" 78 | exit 79 | fi 80 | 81 | first="$1" 82 | shift || : 83 | 84 | if "$pull_tar" "$first"; then 85 | "$pull_tar" "$@" 86 | else 87 | "$import_tar" "$first" "$@" 88 | fi 89 | 90 | have_images=() 91 | while read -r image _; do 92 | have_images[$image]=1 93 | done < <(machinectl --no-pager --no-legend list-images) 94 | 95 | needed_images=() 96 | for image in "${wanted_images[@]}"; do 97 | if [[ "${have_images[$image]:-0}" != 1 ]]; then 98 | needed_images+=("$image") 99 | fi 100 | done 101 | 102 | if (( ${#needed_images[@]} > 0 )); then 103 | echo 1>&2 "Failed to pull images: ${needed_images[*]}" 104 | exit 1 105 | fi 106 | -------------------------------------------------------------------------------- /scripts/download-images-and-spawn-containers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Attempts to infer the names of images to download from looping over mkosi.* 4 | # defaults file names. 5 | 6 | set -euo pipefail 7 | 8 | looks_like_mkosi_dir() { 9 | [[ -d "${1:?whoopsie}" ]] && { [[ -f "${1}/mkosi.default" ]] || [[ -d "${1}/mkosi.files" ]] ; } 10 | } 11 | 12 | if [[ -z "${MKOSI_DIR:-}" ]]; then 13 | candidates=() 14 | toplevel='' 15 | 16 | if command -v git &>/dev/null && toplevel="$(git rev-parse --show-toplevel 2>/dev/null)"; then 17 | candidates+=("${toplevel}/mkosi") 18 | fi 19 | 20 | if toplevel="$(readlink -f "${0%/*}/../mkosi")"; then 21 | candidates+=("${toplevel}/mkosi") 22 | fi 23 | 24 | candidates+=(/vagrant/mkosi /mkosi "$(pwd -P)") 25 | 26 | for candidate in "${candidates[@]}"; do 27 | if looks_like_mkosi_dir "$candidate"; then 28 | MKOSI_DIR="$candidate" 29 | break 30 | fi 31 | done 32 | fi 33 | 34 | shopt -s nullglob 35 | 36 | images=("$@") 37 | mkosi_files="${MKOSI_DIR:-}/mkosi.files" 38 | if [[ -n "${MKOSI_DIR:-}" ]] && [[ -d "$mkosi_files" ]]; then 39 | for mkosi_defaults in "$mkosi_files"/mkosi.*; do 40 | images+=("${mkosi_defaults##*/mkosi.}") 41 | done 42 | fi 43 | 44 | dirname="${BASH_SOURCE[0]%/*}" 45 | 46 | "${dirname}/download-images" "${images[@]}" 47 | exec "${dirname}/spawn-containers" 48 | -------------------------------------------------------------------------------- /scripts/make-ansible-executable: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | for bindir in "$@"; do 6 | [ -d "$bindir" ] || continue 7 | 8 | for ansible_util in "${bindir}/ansible"*; do 9 | if [ "$ansible_util" = "${bindir}/ansible*" ]; then 10 | continue 2 11 | else 12 | break 13 | fi 14 | done 15 | 16 | chmod +x "${bindir}/ansible"* 17 | done 18 | -------------------------------------------------------------------------------- /scripts/resize-machine-btrfs-partition: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Adapted from: 4 | # https://github.com/kinvolk/kube-spawn/blob/692e9a03bace5afe3e7bdcf605b3bf863526745b/pkg/bootstrap/node.go 5 | 6 | set -eu 7 | 8 | MACHINE_MOUNT_PATH="${MACHINE_MOUNT_PATH:-/var/lib/machines}" 9 | MACHINE_RAW_PATH="${MACHINE_RAW_PATH:-${MACHINE_MOUNT_PATH}.raw}" 10 | MACHINE_POOL_SIZE="${MACHINE_POOL_SIZE:-10G}" 11 | 12 | if [ -e "$MACHINE_RAW_PATH" ] && ! machinectl set-limit "$MACHINE_POOL_SIZE"; then 13 | umount "$MACHINE_MOUNT_PATH" || { 14 | case "$?" in 15 | 32) 16 | : 17 | ;; 18 | *) 19 | exit $? 20 | ;; 21 | esac 22 | } 23 | 24 | qemu-img resize -f raw "$MACHINE_RAW_PATH" "$MACHINE_POOL_SIZE" 25 | mount -t btrfs -o loop "$MACHINE_RAW_PATH" "$MACHINE_MOUNT_PATH" 26 | btrfs filesystem resize max "$MACHINE_MOUNT_PATH" 27 | btrfs quota disable "$MACHINE_MOUNT_PATH" 28 | fi 29 | -------------------------------------------------------------------------------- /scripts/run-tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | TEST_DIR='' 6 | 7 | if (( $# > 0 )); then 8 | TEST_DIR="$1" 9 | shift 10 | else 11 | TEST_DIR="$(readlink -f "${BASH_SOURCE[0]%/*}/..")" 12 | fi 13 | 14 | cd "$TEST_DIR" 15 | 16 | # shellcheck disable=SC2030 17 | if (! tty || ! read -r -t 1 -n 0) &>/dev/null; then 18 | coproc socat PTY,link="${PWD}/tty" OPEN:/dev/null,ignoreeof 19 | 20 | cleanup() { 21 | if [[ -n "${COPROC_PID:-}" ]]; then 22 | # shellcheck disable=SC2031 23 | kill "$COPROC_PID" || printf 1>&2 -- '%s: killing socat failed with exit status "%d"\n' "$0" "$?" 24 | fi 25 | } 26 | 27 | trap cleanup exit 28 | 29 | for _ in {1..10}; do 30 | if [[ -e "${PWD}/tty" ]]; then 31 | break 32 | fi 33 | 34 | sleep 1 35 | done 36 | 37 | # Fails and exits if the TTY does not exist 38 | exec 0< "${PWD}/tty" 39 | 40 | tox "$@" 41 | else 42 | exec tox "$@" 43 | fi 44 | -------------------------------------------------------------------------------- /scripts/spawn-containers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | status=0 6 | 7 | while read -r image _; do 8 | if ! machinectl start "$image"; then 9 | systemctl status -l "systemd-nspawn@${image}" 10 | status=1 11 | fi 12 | done < <(machinectl --no-pager --no-legend list-images) 13 | 14 | exit "$status" 15 | -------------------------------------------------------------------------------- /scripts/test-make-hardlinks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | status=0 6 | 7 | for volume in "$@"; do 8 | tmpfile_src='' tmpfile_dest='' 9 | 10 | if tmpfile_src="$(mktemp --tmpdir="$volume" link-probe-src.XXXXXXXXXX)" && tmpfile_dest="$(mktemp -u --tmpdir="$volume" link-probe-dest.XXXXXXXXXX)"; then 11 | if ! ln "$tmpfile_src" "$tmpfile_dest"; then 12 | status=1 13 | fi 14 | 15 | rm -f "${tmpfile_src:?whoopsie}" "${tmpfile_dest:?oh no}" 16 | else 17 | status=1 18 | fi 19 | done 20 | 21 | exit "$status" 22 | -------------------------------------------------------------------------------- /scripts/wait-is-system-running: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | state='' 6 | 7 | # If this bails, systemctl is-system-running doesn't support --wait (new in 8 | # 240) 9 | if ! state="$(systemctl --wait is-system-running)"; then 10 | case "$state" in 11 | initializing|starting) 12 | until state="$(systemctl is-system-running)"; do 13 | case "$state" in 14 | initializing|starting) 15 | sleep 5 16 | ;; 17 | *) 18 | break 19 | ;; 20 | esac 21 | done 22 | ;; 23 | esac 24 | fi 25 | 26 | echo "$state" 27 | -------------------------------------------------------------------------------- /tests/files/directory/foo: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /tests/files/directory/hello: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /tests/files/hello: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | [local] 2 | localhost 3 | 4 | [nspawn] 5 | archlinux-ansible 6 | 7 | [arch:children] 8 | local 9 | nspawn 10 | 11 | [nspawn:vars] 12 | ansible_connection = machinectl 13 | 14 | [arch:vars] 15 | ansible_python_interpreter = /usr/bin/python2 16 | -------------------------------------------------------------------------------- /tests/test.retry: -------------------------------------------------------------------------------- 1 | archlinux-ansible 2 | -------------------------------------------------------------------------------- /tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: create a directory 5 | file: 6 | path: '/etc/foo/bar/baz' 7 | state: directory 8 | - name: run a command 9 | command: date 10 | changed_when: no 11 | - name: read a file 12 | slurp: 13 | path: /etc/machine-id 14 | - name: transfer a file 15 | copy: 16 | src: hello 17 | dest: /tmp/hello 18 | - name: remove a file 19 | file: 20 | dest: /tmp/hello 21 | state: absent 22 | - name: transfer a directory 23 | copy: 24 | src: directory 25 | dest: /tmp 26 | - name: don't clobber the existing directory 27 | copy: 28 | src: directory 29 | dest: /tmp 30 | force: no 31 | - name: check the directory and its contents were not removed 32 | stat: 33 | path: "{{ item }}" 34 | register: stat_result 35 | failed_when: not stat_result.stat.exists 36 | with_items: 37 | - /tmp/directory 38 | - /tmp/directory/hello 39 | - /tmp/directory/foo 40 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.4.0 3 | 4 | # Under Python 3.7, Ansible 2.4 has a syntax error in 5 | # plugins/action/__init__.py, line 96 6 | envlist = 7 | py{27,35,36}-ansible{24,25,26,27,28,29,210} 8 | py{37,38}-ansible{25,26,27,28,29,210} 9 | 10 | skipsdist = true 11 | 12 | skip_missing_interpreters = true 13 | 14 | [travis] 15 | python = 16 | 2.7: py27 17 | 3.4: py34 18 | 3.5: py35 19 | 3.6: py36 20 | 3.7: py37 21 | 3.8: py38 22 | 23 | [travis:env] 24 | ANSIBLE = 25 | 2.4: ansible24 26 | 2.5: ansible25 27 | 2.6: ansible26 28 | 2.7: ansible27 29 | 2.8: ansible28 30 | 2.9: ansible29 31 | 2.10: ansible210 32 | 33 | [testenv] 34 | deps = 35 | coverage 36 | ansible24: ansible>=2.4.0,<2.5.0 37 | ansible25: ansible>=2.5.0,<2.6.0 38 | ansible26: ansible>=2.6.0,<2.7.0 39 | ansible27: ansible>=2.7.0,<2.8.0 40 | ansible28: ansible>=2.8.0,<2.9.0 41 | ansible29: ansible>=2.9.0,<2.10.0 42 | ansible210: ansible>=2.10.0,<2.11.0 43 | 44 | skip_install = true 45 | 46 | whitelist_externals = 47 | pwd 48 | {toxinidir}/scripts/make-ansible-executable 49 | 50 | setenv = 51 | PYTHONPATH = {toxinidir}{:}{env:PYTHONPATH:} 52 | PYTHONUNBUFFERED = 1 53 | PYTHONWARNINGS = default::DeprecationWarning 54 | ANSIBLE_FORCE_COLOR = {tty:1:0} 55 | 56 | passenv = 57 | ANSIBLE_DEBUG 58 | CI 59 | TRAVIS 60 | TRAVIS_BRANCH 61 | TRAVIS_JOB_ID 62 | 63 | # For undetermined reasons, sometimes Ansible is installed without the 64 | # executable bits set on ansible-playbook, etc. 65 | commands_pre = 66 | {toxinidir}/scripts/make-ansible-executable {envbindir} 67 | 68 | commands = 69 | coverage run --source={toxinidir}/connection_plugins --omit={envdir} {envbindir}/ansible-playbook -vv -i ./contrib/inventory ./tests/test.yml 70 | 71 | commands_post = 72 | coverage report 73 | --------------------------------------------------------------------------------