├── .gitignore
├── .gitmodules
├── LICENSE
├── Makefile
├── README.md
├── ansible.cfg
├── defaults
└── main.yml
├── filter_plugins
└── categorize_wirless.py
├── hosts
├── library
└── .gitkeep
├── meta
└── main.yml
├── playbook.yml
├── src
├── BinDecHex.lua
├── ansible.lua
├── ansible_test.lua
├── base64.lua
├── copy.lua
├── dkjson.lua
├── fatpack.pl
├── file.lua
├── fileutils.lua
├── lineinfile.lua
├── opkg.lua
├── ping.lua
├── slurp.lua
├── stat.lua
├── ubus.lua
└── uci.lua
└── test
├── command
├── failing
│ ├── binpath.json
│ └── command.json
└── valid
│ ├── binpath.json
│ ├── binpath_defaults.json
│ ├── command.json
│ └── command_stderr.json
├── failing
├── fail_bool.json
├── fail_choice.json
├── fail_dict.json
├── fail_float.json
├── fail_int.json
├── fail_jsonarg.json
├── fail_list.json
├── fail_path.json
├── fail_req.json
├── fail_string.json
└── fail_unknown.json
└── valid
├── valid.json
├── valid_change.json
├── valid_defaults.json
├── valid_stringly.json
└── valid_stringly2.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .vagrant/
2 | *.pyc
3 | library/*.lua
4 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "test/openwrt-in-vagrant"]
2 | path = test/openwrt-in-vagrant
3 | url = https://github.com/lifeeth/openwrt-in-vagrant
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 |
3 | all: ./library/copy.lua ./library/file.lua ./library/lineinfile.lua ./library/opkg.lua ./library/ping.lua ./library/slurp.lua ./library/stat.lua ./library/ubus.lua ./library/uci.lua
4 |
5 | WHITELIST=io,os,posix.,ubus
6 | FATPACKARGS=--whitelist $(WHITELIST) --truncate
7 |
8 | ./library/%.lua : ./src/%.lua ./src/*.lua
9 | ./src/fatpack.pl --input $^ --output $(dir $@) $(FATPACKARGS)
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Philote
2 |
3 | Ansible orcherstration for Openwrt - with Lua!
4 |
5 | Ansible is build around a collection of modules that get send to the remote
6 | host to execute different tasks or collect information. Those modules are
7 | implemented in python. However on embedded systems such as routers, resources,
8 | in particular flash memory are scarce and a python runtime often not available.
9 |
10 | Those modules communicate with the ansible-toolsuite via well defined interfaces
11 | and are executed via ssh. As each module is a standalone program, there is no
12 | dependency whatsoever on the implementation language. There are existing
13 | attempts like [this](https://github.com/lefant/ansible-openwrt) which already
14 | implement a small set of modules as bash-scripts.
15 |
16 | However the primary author of this project disagrees with some of the
17 | implementation decisions (e.g. sourcing files with key=value-pairs as a kind of
18 | parsing) and is generally a fan of (even rather limited in luas case) typing. So
19 | this project was born.
20 |
21 | As the OpenWrt community seems to have a strange affection for lua, this
22 | repository currently implements the following modules:
23 | - [copy](https://docs.ansible.com/ansible/copy_module.html)
24 | - [file](https://docs.ansible.com/ansible/file_module.html)
25 | - [lineinfile](https://docs.ansible.com/ansible/lineinfile_module.html)
26 | - opkg
27 | - [ping](https://docs.ansible.com/ansible/ping_module.html)
28 | - [stat](https://docs.ansible.com/ansible/stat_module.html)
29 | - ubus
30 | - uci
31 |
32 | Copy, file, lineinfile, stat, ping and opkg are mostly straightforward ports of the
33 | official python modules included in the Ansible v2.1.1.0 release. However, there
34 | were some simplifications made:
35 | - selinux file attributes are not supported
36 | - validation commands are not supported
37 | - file-operations are not guaranteed to be atomic
38 | - permissions can only be specified in octal mode
39 | - check_mode is only partly implemented
40 |
41 | Apart from that, the modules should behave exactly like the upstream modules,
42 | making it possible to use local actions such as
43 | "[template](https://docs.ansible.com/ansible/template_module.html)" which are
44 | built upon those modules.
45 |
46 | # Requirements
47 |
48 | For building the modules, perl and the
49 | [Data::Compare](http://search.cpan.org/~dcantrell/Data-Compare-1.25/lib/Data/Compare.pm)
50 | library are required.
51 |
52 | If you want to use the file related modules (copy, file, lineinfile, stat), the
53 | following opkg packages are required, which are not part of the standard images:
54 | - luaposix
55 | - coreutils-sha1sum
56 |
57 | However, as the opkg-module is independent from those packages, you can install
58 | them in your playbook like this:
59 |
60 | ```yaml
61 | - name: Installing dependencies for file-related modules
62 | opkg: pkg=luaposix,coreutils-sha1sum state=present update_cache=yes
63 | ```
64 |
65 | # Building/Installation
66 |
67 | Ansible currently has no notion of libraries used within modules (only limited
68 | support for ansibles own core python libraries is available). For more
69 | information please see
70 | [this issue](https://github.com/ansible/ansible/pull/10274). Therefore all
71 | modules that should be used have to be fatpacked (that is, the module all
72 | referenced libraries have to be packed into one giant lua script). This is done
73 | by the [fatpack.pl](./src/fatpack.pl) script. Usage is like this:
74 |
75 | ```bash
76 | ./src/fatpack.pl --input .lua --output ./library/ --whitelist
77 | io,os,posix.,ubus --truncate
78 | ```
79 |
80 | To make this process easier, a Makefile is provided that packs all modules in
81 | `./src/` and places the fatpacked variants in `library` for you. Just run `make`
82 | in the projects top directory.
83 |
84 | Please note, that this project is currently in **alpha** state. I used it to manage
85 | my personal router (playbook coming soon), but it still might easily lock you
86 | out of your device, eat your hamsters or worse. So please check your playbook
87 | beforehand against a VM (e.g. the one from the openwrt-vagrant project which
88 | can be built from the submodule in `./test/`) or be sure that your router has a
89 | convenient reset/failsafe path.
90 |
91 | Apart form the `./library/` folder, you might want to copy the provided `ansible.cfg` as it configures ansible for better interoperability with the dropbear ssh-daemon used by openwrt.
92 |
93 | # Documentation
94 |
95 | For the following modules, please refer to the upstream documentation
96 | - [copy](https://docs.ansible.com/ansible/copy_module.html)
97 | - [file](https://docs.ansible.com/ansible/file_module.html)
98 | - [lineinfile](https://docs.ansible.com/ansible/lineinfile_module.html)
99 | - [opkg](https://docs.ansible.com/ansible/opkg_module.html)
100 | - [ping](https://docs.ansible.com/ansible/ping_module.html)
101 | - [stat](https://docs.ansible.com/ansible/stat_module.html)
102 |
103 | ## ubus module
104 |
105 | As a replacement for then official setup module, information on the openwrt
106 | system can be gatherd via the ubus interface and will automatically be
107 | integrated into the host_facts for reuse in the playbook like this:
108 |
109 | ```yaml
110 | ubus: cmd=facts
111 | ```
112 |
113 | Otherwise, this module is a slim wrapper around the
114 | [ubus rpc-bus](https://wiki.openwrt.org/doc/techref/ubus).
115 |
116 | For a list of available ubus-service-providers and their functions, you can
117 | issue a list call. Please note that this call is not really useful in an
118 | automated setting:
119 | ```bash
120 | $ ansible openwrt -i hosts -m ubus -a 'cmd=list'
121 | openwrt | SUCCESS => {
122 | "changed": false,
123 | "invocations": {
124 | "module_args": {
125 | "command": "list"
126 | }
127 | },
128 | "msg": "Gathered local signatures",
129 | "signatures": {
130 | [...]
131 | "uci": {
132 | [...]
133 | "get": {
134 | "config": 3,
135 | "match": 2,
136 | "option": 3,
137 | "section": 3,
138 | "type": 3,
139 | "ubus_rpc_session": 3
140 | },
141 | [...]
142 | },
143 | [...]
144 | }
145 | }
146 | ```
147 |
148 | Those signatures can then be used to make Calls via ubus:
149 |
150 | ```yaml
151 | ubus: cmd=call path=uci method=get message='{"config":"uhttpd", "section":"main", "option":"listen_http"}"'
152 | ```
153 |
154 | As you can see, the `ubus_rpc_session` parameter is automatically inserted for
155 | you by the module. The ubus return value is returned in the `result` field of the returned object and can be accessed like this:
156 |
157 | ```yaml
158 | - name: Query http listen ports
159 | ubus: cmd=call path=uci method=get message='{"config":"uhttpd", "section":"main", "option":"listen_http"}"'
160 | register: foo
161 |
162 | - name: Do something
163 | baz: param={{ result.value }}
164 | ```
165 |
166 | ## UCI-Module
167 |
168 | As most ubus calls will most likely target the
169 | [uci-system](https://wiki.openwrt.org/doc/uci) a dedicated module/ubus-wrapper
170 | for the uci configuration is provided. Basic familiarity with uci is assumed, so
171 | please refer to the upstream [documentation](https://wiki.openwrt.org/doc/uci)
172 | otherwise. Most of the options should map quite naturally to the module
173 | parameters:
174 |
175 | A special warning about types: UCI has two types for values internally: `list`
176 | and `option`. The module tries to infer the type by looking for `,` in the
177 | input. If you need to force a singleentry list, please be sure to set the
178 | `forcelist=yes` parameter.
179 |
180 | | parameter | required | default | choices | comments |
181 | |-----------|----------|---------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
182 | | name | no | | | Path to the property to change. Syntax is `config.section.option`. _Aliases: path, key_ |
183 | | value | no | | | For set: value to set the property to |
184 | | match | no | | | When present in a set or get op: properties a section must have to be modified or returned |
185 | | values | no | | | For set with match: values to set on matching section |
186 | | forcelist | no | false | Boolean | The module trys to guess the uci config type (list or string) from the supplied value via the existance of `,` in the input. Single entry lists require `forcelist=yes` to be recognized correctly |
187 | | state | no | present | present, absent, set, unset | State of the property |
188 | | op | no | | configs, commit, revert, get| If specified, instead of enforcing a value, either list the available configurations, execute a commit/revert operation, or query properties. |
189 | | reload | no | | Boolean | Whether to reload the configuration from disk before executing. _Aliases: reload_configs, reload-configs_ |
190 | | autocomit | no | true | Boolean | Whether to automatically commit the changes made |
191 | | type | no | | | When creating a new section, a configuration-type is required. Can also be used to qualify a get. Aliases: _section-type_ |
192 | | socket | no | | | Set a nonstandard path to the ubus socket if necessary |
193 | | timeout | no | | | Change the default ubus timeout |
194 |
195 | Examples:
196 |
197 | ```yaml
198 | # Set a value
199 | uci: name="system.@system[0].hostname" value="mysuperduperrouter"
200 |
201 | # Delete a value
202 | uci: name="system.@system[0].hostname" state=absent
203 |
204 | # Revert and commit globally
205 | uci: op=revert
206 | uci: op=commit
207 |
208 | # Only commit/revert a single section
209 | uci: path=dropbear op=revert
210 | uci: path=dropbear op=commit
211 |
212 | # Create the uhttpd.test section with type uhttp
213 | # and set foo=bar
214 | uci: name=uhttpd.test.foo value=bar type="uhttpd" autocommit=false'
215 |
216 | # Remove the uttpd.test section
217 | uci: name=uhttpd.test state="absent" autocommit=true'
218 |
219 | # Get a list of all available configuration files
220 | uci: op=configs
221 | ```
222 |
223 | An more complex example showing the usage of forcelist:
224 |
225 | ```yaml
226 | - name: Securing uhttpd - Disable listening on wan
227 | uci: name={{ item.key }} value={{ uci.state.network.lan.ipaddr }}:{{ item.port }} forcelist=true autocommit=false
228 | with_items:
229 | - { key: 'uhttpd.main.listen_http', port: '80' }
230 | - { key: 'uhttpd.main.listen_https', port: '443' }
231 | notify:
232 | - uci commit
233 | ```
234 |
235 | # Contributing
236 |
237 | Give me all your pullrequests :) If you find a bug in one of the provided modules
238 | (quite possible) or want to contribute a new module, feel free to propose a
239 | pullrequest.
240 | To make development of the modules easier, two libraries are provided. The
241 | ansible library in `./src/ansible.lua` tries to provide a easy starting point
242 | for module development similar to ansibles `ansible.module_utils.basic` library.
243 |
244 | It will handle argument parsing for you:
245 |
246 | ```lua
247 | local module = Ansible.new({
248 | name = { aliases = {"pkg"}, required=true , type='list'},
249 | state = { default = "present", choices={"present", "installed", "absent", "removed"} },
250 | force = { default = "", choices={"", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"} } ,
251 | update_cache = { default = "no", aliases={ "update-cache" }, type='bool' }
252 | })
253 |
254 | module:parse(arg[1])
255 |
256 | local p = module:get_params()
257 | ```
258 |
259 | And provides some convenience function such as `get_bin_path`, `run_command`,
260 | `fail_json` and `exit_json`. Currently, those are badly underdocumented, but
261 | the names are mostly selfexplanatory, so just look through the functions in the
262 | file.
263 |
264 | ```lua
265 | local opkg_path = module:get_bin_path('echo', true, {'/bin'})
266 | local rc, out, err = module:run_command(string.format("%s foobar", opkg_path))
267 | if rc ~= 0 then
268 | module:fail_json({msg="failed to echo foobar", info={rc=rc, out=out, err=err}})
269 | else
270 | module:exit_json({msg="successfully echod foobar", changed=false})
271 | end
272 | ```
273 |
274 | Additionally, the `./src/fileutils.lua` module has various wrappers for various
275 | filesystemrelated tasks. Again: Please look up the functions in the sourcefile
276 | and look how they are used in the provided modules.
277 |
278 | # License
279 |
280 | The libraries and submodules were only included in this repository for
281 | convenience and are available under their own respective licenses:
282 | - [dkjson](http://dkolf.de/src/dkjson-lua.fsl/home) MIT License
283 | - [BinDecHex](http://www.dialectronics.com/Lua/code/BinDecHex.shtml) MIT License
284 | - [openwrt-in-vagrant](https://github.com/lifeeth/openwrt-in-vagrant) MIT License
285 |
286 | All other code is available under the terms and conditions of the AGPL3 license.
287 | For more details please see the [LICENSE file](LICENSE).
288 |
289 | # Trivia
290 |
291 | In Orson Scott Cards marvellous Ender's Game series the term "ansible" refers to
292 | a device for faster than light communication. The philote is the (fictional)
293 | subatomic particle which delivers the actual messages.
294 |
--------------------------------------------------------------------------------
/ansible.cfg:
--------------------------------------------------------------------------------
1 | [ssh_connection]
2 | ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o "MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1,hmac-md5" -o "KexAlgorithms diffie-hellman-group-exchange-sha256,diffie-hellman-group1-sha1,diffie-hellman-group14-sha1"
3 | scp_if_ssh=True
4 |
--------------------------------------------------------------------------------
/defaults/main.yml:
--------------------------------------------------------------------------------
1 | ansible_remote_tmp: "/tmp/ansible" # Reduce flash wear on target device
2 |
--------------------------------------------------------------------------------
/filter_plugins/categorize_wirless.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 |
3 | import unittest
4 | from ansible import errors
5 |
6 | def categorize_wireless(uciwireless):
7 | try:
8 | # Basically, we've got a list with tags. All tags can be reconstructed with the ".name" attribute
9 | vals = uciwireless.values()
10 | devs = filter(lambda x: x['.type'] == "wifi-device", vals)
11 | ifaces = filter(lambda x: x['.type'] == "wifi-iface", vals)
12 |
13 | configs = { "all" : [] }
14 | for iface in ifaces:
15 | devname = iface['device']
16 | dev = uciwireless[devname]
17 | hwmode = dev['hwmode']
18 | configs.setdefault(hwmode, [])
19 | transformed = {
20 | "device": dev['.name'],
21 | "htmode": dev['htmode'],
22 | "hwmode": dev['hwmode'],
23 | "iface": iface['.name']
24 | }
25 | configs[hwmode].append(transformed)
26 | configs['all'].append(transformed)
27 |
28 | return configs
29 | except Exception as e:
30 | raise errors.AnsibleFilterError(
31 | 'categorize_wireless plugin error: {0}, uciwireless={1},'
32 | ''.format(str(e), str(uciwireless)))
33 |
34 | class FilterModule(object):
35 | ''' Categorize wireless nics and the associated aps '''
36 | def filters(self):
37 | return {
38 | 'categorize_wireless': categorize_wireless
39 | }
40 |
41 |
42 | class TestWirelessFilters(unittest.TestCase):
43 | def test_categorize_wireless(self):
44 | testdata = {
45 | "cfg033579": {
46 | ".anonymous": True,
47 | ".index": 1,
48 | ".name": "cfg033579",
49 | ".type": "wifi-iface",
50 | "device": "radio0",
51 | "encryption": "none",
52 | "mode": "ap",
53 | "network": "lan",
54 | "ssid": "OpenWrt"
55 | },
56 | "cfg063579": {
57 | ".anonymous": True,
58 | ".index": 3,
59 | ".name": "cfg063579",
60 | ".type": "wifi-iface",
61 | "device": "radio1",
62 | "encryption": "none",
63 | "mode": "ap",
64 | "network": "lan",
65 | "ssid": "OpenWrt"
66 | },
67 | "radio0": {
68 | ".anonymous": False,
69 | ".index": 0,
70 | ".name": "radio0",
71 | ".type": "wifi-device",
72 | "channel": "36",
73 | "disabled": "1",
74 | "htmode": "VHT80",
75 | "hwmode": "11a",
76 | "path": "pci0000:00\/0000:00:00.0\/0000:01:00.0",
77 | "type": "mac80211"
78 | },
79 | "radio1": {
80 | ".anonymous": False,
81 | ".index": 2,
82 | ".name": "radio1",
83 | ".type": "wifi-device",
84 | "channel": "11",
85 | "disabled": "1",
86 | "htmode": "HT20",
87 | "hwmode": "11g",
88 | "path": "pci0000:00\/0000:00:01.0\/0000:02:00.0",
89 | "type": "mac80211"
90 | }
91 | }
92 | expected = {
93 | "11a": [{"device" : "radio0", "htmode": "VHT80", "iface": "cfg033579", "hwmode": "11a"}],
94 | "11g": [{"device" : "radio1", "htmode": "HT20", "iface" : "cfg063579", "hwmode": "11g"}],
95 | "all": [{"device" : "radio0", "htmode": "VHT80", "iface": "cfg033579", "hwmode": "11a"},
96 | {"device" : "radio1", "htmode": "HT20", "iface" : "cfg063579", "hwmode": "11g"}]
97 | }
98 | self.assertDictEqual(categorize_wireless(testdata), expected)
99 |
100 | if __name__ == '__main__':
101 | unittest.main()
102 |
--------------------------------------------------------------------------------
/hosts:
--------------------------------------------------------------------------------
1 | openwrt ansible_port=2222 ansible_host=127.0.0.1 ansible_user=root ansible_password=root
2 |
--------------------------------------------------------------------------------
/library/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noctux/philote/24a7e44fa0a377a3f7ffd4f4e4e1cc6a9cf39a10/library/.gitkeep
--------------------------------------------------------------------------------
/meta/main.yml:
--------------------------------------------------------------------------------
1 | dependencies: []
2 |
--------------------------------------------------------------------------------
/playbook.yml:
--------------------------------------------------------------------------------
1 | - hosts: all
2 | gather_facts: False
3 | tasks:
4 | - name: Gathering facts
5 | ubus: cmd=facts
6 |
7 | - name: Installing dependencies for file-related modules
8 | opkg: pkg=luaposix,coreutils-sha1sum state=present update_cache=yes
9 |
10 | - name: Securing uhttpd - Disable listening on wan
11 | uci: name={{ item.key }} value={{ uci.state.network.lan.ipaddr }}:{{ item.port }} autocommit=false
12 | with_items:
13 | - { key: 'uhttpd.main.listen_http', port: '80' }
14 | - { key: 'uhttpd.main.listen_https', port: '443' }
15 | notify:
16 | - uci commit
17 | - uhttp reload
18 | - name: Securing dropbear - Disable login from wan
19 | uci: name=dropbear.@dropbear[0].Interface value=br-lan autocommit=false
20 | notify:
21 | - uci commit
22 | - dropbear reload
23 |
24 | handlers:
25 | - name: uci commit
26 | raw: uci commit
27 | - name: uhttp reload
28 | raw: /etc/init.d/uhttpd reload
29 | - name: dropbear reload
30 | raw: /etc/init.d/dropbear reload
31 |
32 |
--------------------------------------------------------------------------------
/src/BinDecHex.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | /*
3 | * Copyright (c) 2007 Tim Kelly/Dialectronics
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the
7 | * "Software"), to deal in the Software without restriction, including
8 | * without limitation the rights to use, copy, modify, merge, publish,
9 | * distribute, sublicense, and/or sell copies of the Software, and to permit
10 | * persons to whom the Software is furnished to do so, subject to the
11 | * following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be
14 | * included in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
21 | * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
22 | * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | */
24 |
25 | --]]
26 |
27 | --[[
28 | /*
29 | * Copyright (c) 2007 Tim Kelly/Dialectronics
30 | *
31 | * Permission is hereby granted, free of charge, to any person obtaining
32 | * a copy of this software and associated documentation files (the
33 | * "Software"), to deal in the Software without restriction, including
34 | * without limitation the rights to use, copy, modify, merge, publish,
35 | * distribute, sublicense, and/or sell copies of the Software, and to permit
36 | * persons to whom the Software is furnished to do so, subject to the
37 | * following conditions:
38 | *
39 | * The above copyright notice and this permission notice shall be
40 | * included in all copies or substantial portions of the Software.
41 | *
42 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
43 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
47 | * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
48 | * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
49 | */
50 |
51 | /* Thanks to Bernard Clabots for string.gfind to make forward compatible to Lua 5.2 */
52 |
53 | --]]
54 |
55 | module(..., package.seeall);
56 |
57 | string.gfind = string.gfind or string.gmatch
58 |
59 | local hex2bin = {
60 | ["0"] = "0000",
61 | ["1"] = "0001",
62 | ["2"] = "0010",
63 | ["3"] = "0011",
64 | ["4"] = "0100",
65 | ["5"] = "0101",
66 | ["6"] = "0110",
67 | ["7"] = "0111",
68 | ["8"] = "1000",
69 | ["9"] = "1001",
70 | ["a"] = "1010",
71 | ["b"] = "1011",
72 | ["c"] = "1100",
73 | ["d"] = "1101",
74 | ["e"] = "1110",
75 | ["f"] = "1111"
76 | }
77 |
78 |
79 |
80 | local bin2hex = {
81 | ["0000"] = "0",
82 | ["0001"] = "1",
83 | ["0010"] = "2",
84 | ["0011"] = "3",
85 | ["0100"] = "4",
86 | ["0101"] = "5",
87 | ["0110"] = "6",
88 | ["0111"] = "7",
89 | ["1000"] = "8",
90 | ["1001"] = "9",
91 | ["1010"] = "A",
92 | ["1011"] = "B",
93 | ["1100"] = "C",
94 | ["1101"] = "D",
95 | ["1110"] = "E",
96 | ["1111"] = "F"
97 | }
98 |
99 | --[[
100 | local dec2hex = {
101 | ["0"] = "0",
102 | ["1"] = "1",
103 | ["2"] = "2",
104 | ["3"] = "3",
105 | ["4"] = "4",
106 | ["5"] = "5",
107 | ["6"] = "6",
108 | ["7"] = "7",
109 | ["8"] = "8",
110 | ["9"] = "9",
111 | ["10"] = "A",
112 | ["11"] = "B",
113 | ["12"] = "C",
114 | ["13"] = "D",
115 | ["14"] = "E",
116 | ["15"] = "F"
117 | }
118 | --]]
119 |
120 |
121 | -- These functions are big-endian and take up to 32 bits
122 |
123 | -- Hex2Bin
124 | -- Bin2Hex
125 | -- Hex2Dec
126 | -- Dec2Hex
127 | -- Bin2Dec
128 | -- Dec2Bin
129 |
130 |
131 | function Hex2Bin(s)
132 |
133 | -- s -> hexadecimal string
134 |
135 | local ret = ""
136 | local i = 0
137 |
138 |
139 | for i in string.gfind(s, ".") do
140 | i = string.lower(i)
141 |
142 | ret = ret..hex2bin[i]
143 |
144 | end
145 |
146 | return ret
147 | end
148 |
149 |
150 | function Bin2Hex(s)
151 |
152 | -- s -> binary string
153 |
154 | local l = 0
155 | local h = ""
156 | local b = ""
157 | local rem
158 |
159 | l = string.len(s)
160 | rem = l % 4
161 | l = l-1
162 | h = ""
163 |
164 | -- need to prepend zeros to eliminate mod 4
165 | if (rem > 0) then
166 | s = string.rep("0", 4 - rem)..s
167 | end
168 |
169 | for i = 1, l, 4 do
170 | b = string.sub(s, i, i+3)
171 | h = h..bin2hex[b]
172 | end
173 |
174 | return h
175 |
176 | end
177 |
178 |
179 | function Bin2Dec(s)
180 |
181 | -- s -> binary string
182 |
183 | local num = 0
184 | local ex = string.len(s) - 1
185 | local l = 0
186 |
187 | l = ex + 1
188 | for i = 1, l do
189 | b = string.sub(s, i, i)
190 | if b == "1" then
191 | num = num + 2^ex
192 | end
193 | ex = ex - 1
194 | end
195 |
196 | return string.format("%u", num)
197 |
198 | end
199 |
200 |
201 |
202 | function Dec2Bin(s, num)
203 |
204 | -- s -> Base10 string
205 | -- num -> string length to extend to
206 |
207 | local n
208 |
209 | if (num == nil) then
210 | n = 0
211 | else
212 | n = num
213 | end
214 |
215 | s = string.format("%x", s)
216 |
217 | s = Hex2Bin(s)
218 |
219 | while string.len(s) < n do
220 | s = "0"..s
221 | end
222 |
223 | return s
224 |
225 | end
226 |
227 |
228 |
229 |
230 | function Hex2Dec(s)
231 |
232 | -- s -> hexadecimal string
233 |
234 | local s = Hex2Bin(s)
235 |
236 | return Bin2Dec(s)
237 |
238 | end
239 |
240 |
241 |
242 | function Dec2Hex(s)
243 |
244 | -- s -> Base10 string
245 |
246 | s = string.format("%x", s)
247 |
248 | return s
249 |
250 | end
251 |
252 |
253 |
254 |
255 | -- These functions are big-endian and will extend to 32 bits
256 |
257 | -- BMAnd
258 | -- BMNAnd
259 | -- BMOr
260 | -- BMXOr
261 | -- BMNot
262 |
263 |
264 | function BMAnd(v, m)
265 |
266 | -- v -> hex string to be masked
267 | -- m -> hex string mask
268 |
269 | -- s -> hex string as masked
270 |
271 | -- bv -> binary string of v
272 | -- bm -> binary string mask
273 |
274 | local bv = Hex2Bin(v)
275 | local bm = Hex2Bin(m)
276 |
277 | local i = 0
278 | local s = ""
279 |
280 | while (string.len(bv) < 32) do
281 | bv = "0000"..bv
282 | end
283 |
284 | while (string.len(bm) < 32) do
285 | bm = "0000"..bm
286 | end
287 |
288 |
289 | for i = 1, 32 do
290 | cv = string.sub(bv, i, i)
291 | cm = string.sub(bm, i, i)
292 | if cv == cm then
293 | if cv == "1" then
294 | s = s.."1"
295 | else
296 | s = s.."0"
297 | end
298 | else
299 | s = s.."0"
300 |
301 | end
302 | end
303 |
304 | return Bin2Hex(s)
305 |
306 | end
307 |
308 |
309 | function BMNAnd(v, m)
310 |
311 | -- v -> hex string to be masked
312 | -- m -> hex string mask
313 |
314 | -- s -> hex string as masked
315 |
316 | -- bv -> binary string of v
317 | -- bm -> binary string mask
318 |
319 | local bv = Hex2Bin(v)
320 | local bm = Hex2Bin(m)
321 |
322 | local i = 0
323 | local s = ""
324 |
325 | while (string.len(bv) < 32) do
326 | bv = "0000"..bv
327 | end
328 |
329 | while (string.len(bm) < 32) do
330 | bm = "0000"..bm
331 | end
332 |
333 |
334 | for i = 1, 32 do
335 | cv = string.sub(bv, i, i)
336 | cm = string.sub(bm, i, i)
337 | if cv == cm then
338 | if cv == "1" then
339 | s = s.."0"
340 | else
341 | s = s.."1"
342 | end
343 | else
344 | s = s.."1"
345 |
346 | end
347 | end
348 |
349 | return Bin2Hex(s)
350 |
351 | end
352 |
353 |
354 |
355 | function BMOr(v, m)
356 |
357 | -- v -> hex string to be masked
358 | -- m -> hex string mask
359 |
360 | -- s -> hex string as masked
361 |
362 | -- bv -> binary string of v
363 | -- bm -> binary string mask
364 |
365 | local bv = Hex2Bin(v)
366 | local bm = Hex2Bin(m)
367 |
368 | local i = 0
369 | local s = ""
370 |
371 | while (string.len(bv) < 32) do
372 | bv = "0000"..bv
373 | end
374 |
375 | while (string.len(bm) < 32) do
376 | bm = "0000"..bm
377 | end
378 |
379 |
380 | for i = 1, 32 do
381 | cv = string.sub(bv, i, i)
382 | cm = string.sub(bm, i, i)
383 | if cv == "1" then
384 | s = s.."1"
385 | elseif cm == "1" then
386 | s = s.."1"
387 | else
388 | s = s.."0"
389 | end
390 | end
391 |
392 | return Bin2Hex(s)
393 |
394 | end
395 |
396 | function BMXOr(v, m)
397 |
398 | -- v -> hex string to be masked
399 | -- m -> hex string mask
400 |
401 | -- s -> hex string as masked
402 |
403 | -- bv -> binary string of v
404 | -- bm -> binary string mask
405 |
406 | local bv = Hex2Bin(v)
407 | local bm = Hex2Bin(m)
408 |
409 | local i = 0
410 | local s = ""
411 |
412 | while (string.len(bv) < 32) do
413 | bv = "0000"..bv
414 | end
415 |
416 | while (string.len(bm) < 32) do
417 | bm = "0000"..bm
418 | end
419 |
420 |
421 | for i = 1, 32 do
422 | cv = string.sub(bv, i, i)
423 | cm = string.sub(bm, i, i)
424 | if cv == "1" then
425 | if cm == "0" then
426 | s = s.."1"
427 | else
428 | s = s.."0"
429 | end
430 | elseif cm == "1" then
431 | if cv == "0" then
432 | s = s.."1"
433 | else
434 | s = s.."0"
435 | end
436 | else
437 | -- cv and cm == "0"
438 | s = s.."0"
439 | end
440 | end
441 |
442 | return Bin2Hex(s)
443 |
444 | end
445 |
446 |
447 | function BMNot(v, m)
448 |
449 | -- v -> hex string to be masked
450 | -- m -> hex string mask
451 |
452 | -- s -> hex string as masked
453 |
454 | -- bv -> binary string of v
455 | -- bm -> binary string mask
456 |
457 | local bv = Hex2Bin(v)
458 | local bm = Hex2Bin(m)
459 |
460 | local i = 0
461 | local s = ""
462 |
463 | while (string.len(bv) < 32) do
464 | bv = "0000"..bv
465 | end
466 |
467 | while (string.len(bm) < 32) do
468 | bm = "0000"..bm
469 | end
470 |
471 |
472 | for i = 1, 32 do
473 | cv = string.sub(bv, i, i)
474 | cm = string.sub(bm, i, i)
475 | if cm == "1" then
476 | if cv == "1" then
477 | -- turn off
478 | s = s.."0"
479 | else
480 | -- turn on
481 | s = s.."1"
482 | end
483 | else
484 | -- leave untouched
485 | s = s..cv
486 |
487 | end
488 | end
489 |
490 | return Bin2Hex(s)
491 |
492 | end
493 |
494 |
495 | -- these functions shift right and left, adding zeros to lost or gained bits
496 | -- returned values are 32 bits long
497 |
498 | -- BShRight(v, nb)
499 | -- BShLeft(v, nb)
500 |
501 |
502 | function BShRight(v, nb)
503 |
504 | -- v -> hexstring value to be shifted
505 | -- nb -> number of bits to shift to the right
506 |
507 | -- s -> binary string of v
508 |
509 | local s = Hex2Bin(v)
510 |
511 | while (string.len(s) < 32) do
512 | s = "0000"..s
513 | end
514 |
515 | s = string.sub(s, 1, 32 - nb)
516 |
517 | while (string.len(s) < 32) do
518 | s = "0"..s
519 | end
520 |
521 | return Bin2Hex(s)
522 |
523 | end
524 |
525 | function BShLeft(v, nb)
526 |
527 | -- v -> hexstring value to be shifted
528 | -- nb -> number of bits to shift to the right
529 |
530 | -- s -> binary string of v
531 |
532 | local s = Hex2Bin(v)
533 |
534 | while (string.len(s) < 32) do
535 | s = "0000"..s
536 | end
537 |
538 | s = string.sub(s, nb + 1, 32)
539 |
540 | while (string.len(s) < 32) do
541 | s = s.."0"
542 | end
543 |
544 | return Bin2Hex(s)
545 |
546 | end
547 |
--------------------------------------------------------------------------------
/src/ansible.lua:
--------------------------------------------------------------------------------
1 | local Ansible = {}
2 |
3 | local io = require("io")
4 | local json = require("dkjson")
5 | local ubus = require("ubus")
6 |
7 | Ansible.__index = Ansible
8 |
9 | local json_arguments = [===[<>]===]
10 |
11 | function Ansible.new(spec)
12 | local self = setmetatable({}, Ansible)
13 | self.spec = spec
14 | for k,v in pairs(spec) do
15 | v['name'] = k
16 | end
17 | self.params = nil
18 | return self
19 | end
20 |
21 | local function split(str, delimiter)
22 | local toks = {}
23 |
24 | for tok in string.gmatch(str, "[^".. delimiter .. "]+") do
25 | toks[#toks + 1] = tok
26 | end
27 |
28 | return toks
29 | end
30 |
31 | local function append(t1, t2)
32 | for k,v in ipairs(t2) do
33 | t1[#t1 + 1] = v
34 | end
35 | return t1
36 | end
37 |
38 | function Ansible.contains(needle, haystack)
39 | for _,v in pairs(haystack) do
40 | if needle == v then
41 | return true
42 | end
43 | end
44 |
45 | return false
46 | end
47 |
48 | local function findspec(name, spec)
49 | if spec[name] then
50 | return spec[name]
51 | end
52 |
53 | -- check whether an alias exists
54 | for k,v in pairs(spec) do
55 | if type(v) == "table" and v['aliases'] then
56 | if Ansible.contains(name, v['aliases']) then
57 | return v
58 | end
59 | end
60 | end
61 |
62 | return nil
63 | end
64 |
65 | local function starts_with(str, start)
66 | return str:sub(1, #start) == start
67 | end
68 |
69 | local function extract_internal_ansible_params(params)
70 | local copy = {}
71 | for k,v in pairs(params) do
72 | if starts_with(k, "_ansible") then
73 | copy[k] = v
74 | end
75 | end
76 | return copy
77 | end
78 |
79 | local function canonicalize(params, spec)
80 | local copy = {}
81 | for k,v in pairs(params) do
82 | local desc = findspec(k, spec)
83 | if not desc then
84 | -- ignore _ansible parameters
85 | if 1 ~= string.find(k, "_ansible") then
86 | return nil, "no such parameter " .. k
87 | end
88 | else
89 | if copy[desc['name']] then
90 | return nil, "duplicate parameter " .. desc['name']
91 | end
92 | copy[desc['name']] = v
93 | end
94 | end
95 |
96 | params = copy
97 |
98 | return copy
99 | end
100 |
101 | function Ansible:slurp(path)
102 | local f, err = io.open(path, "r")
103 | if f == nil then
104 | Ansible.fail_json({msg="failed to open file " .. path .. ": " .. err})
105 | end
106 | local content = f:read("*a")
107 | if content == nil then
108 | self:fail_json({msg="read from file " .. path .. "failed"})
109 | end
110 | f:close()
111 | return content
112 | end
113 |
114 | function Ansible:unslurp(path, content)
115 | local f, err = io.open(path, "w+")
116 | if f == nil then
117 | Ansible.fail_json({msg="failed to open file " .. path .. ": " .. err})
118 | end
119 |
120 | local res = f:write(content)
121 |
122 | if not res then
123 | self:fail_json({msg="read from file " .. path .. "failed"})
124 | end
125 | f:close()
126 | return res
127 | end
128 |
129 | local function parse_dict_from_string(str)
130 | if 1 == string.find(str, "{") then
131 | -- assume json, try to decode it
132 | local dict, pos, err = json.decode(str)
133 | if not err then
134 | return dict
135 | end
136 | elseif string.find(str, "=") then
137 | fields = {}
138 | field_buffer = ""
139 | in_quote = nil
140 | in_escape = false
141 | for c in str:gmatch(".") do
142 | if in_escape then
143 | field_buffer = field_buffer .. c
144 | in_escape = false
145 | elseif c == '\\' then
146 | in_escape = true
147 | elseif not in_quote and ('\'' == c or '"' == c) then
148 | in_quote = c
149 | elseif in_quote and in_quote == c then
150 | in_quote = nil
151 | elseif not in_quote and (',' == c or ' ' == c) then
152 | if string.len(field_buffer) > 0 then
153 | fields[#fields + 1] = field_buffer
154 | end
155 | field_buffer=""
156 | else
157 | field_buffer = field_buffer .. c
158 | end
159 | end
160 | -- append the final field
161 | fields[#fields + 1] = field_buffer
162 |
163 | local dict = {}
164 |
165 | for _,v in ipairs(fields) do
166 | local key, val = string.match(v, "^([^=]+)=(.*)")
167 |
168 | if key and val then
169 | dict[key] = val
170 | end
171 | end
172 |
173 | return dict
174 | end
175 |
176 | return nil, str .. " dictionary requested, could not parse JSON or key=value"
177 | end
178 |
179 | local function check_transform_type(variable, ansibletype)
180 | -- Types: str list dict bool int float path raw jsonarg
181 | if "str" == ansibletype then
182 | if type(variable) == "string" then
183 | return variable
184 | end
185 | elseif "list" == ansibletype then
186 | if type(variable) == "table" then
187 | return variable
188 | end
189 |
190 | if type(variable) == "string" then
191 | return split(variable, ",")
192 | elseif type(variable) == "number" then
193 | return {variable}
194 | end
195 | elseif "dict" == ansibletype then
196 | if type(variable) == "table" then
197 | return variable
198 | elseif type(variable) == "string" then
199 | return parse_dict_from_string(variable)
200 | end
201 | elseif "bool" == ansibletype then
202 | if "boolean" == type(variable) then
203 | return variable
204 | elseif "number" == type(variable) then
205 | return not (0 == variable)
206 | elseif "string" == type(variable) then
207 | local BOOLEANS_TRUE = {'yes', 'on', '1', 'true', 'True'}
208 | local BOOLEANS_FALSE = {'no', 'off', '0', 'false', 'False'}
209 |
210 | if Ansible.contains(variable, BOOLEANS_TRUE) then
211 | return true
212 | elseif Ansible.contains(variable, BOOLEANS_FALSE) then
213 | return false
214 | end
215 | end
216 | elseif "int" == ansibletype or "float" == ansibletype then
217 | if type(variable) == "string" then
218 | local var = tonumber(variable)
219 | if var then
220 | return var
221 | end
222 | elseif type(variable) == "number" then
223 | return variable
224 | end
225 | elseif "path" == ansibletype then
226 | -- A bit basic, i know
227 | if type(variable) == "string" then
228 | return variable
229 | end
230 | elseif "raw" == ansibletype then
231 | return variable
232 | elseif "jsonarg" == ansibletype then
233 | if "table" == type(variable) then
234 | return variable
235 | elseif "string" == type(variable) then
236 | local dict, pos, err = json.decode(variable)
237 | if not err then
238 | return dict
239 | end
240 | end
241 | else
242 | return nil, ansibletype .. " is not a known type"
243 | end
244 |
245 | return nil, tostring(variable) .. " does not conform to type " .. ansibletype
246 | end
247 |
248 | function Ansible:parse(inputfile)
249 | local params, pos, err = json.decode(json_arguments)
250 |
251 | if err then
252 | self:fail_json({msg="INTERNAL: Illegal json input received"})
253 | end
254 |
255 | self.internal_params = extract_internal_ansible_params(params)
256 | self._diff = self.internal_params['_ansible_diff']
257 |
258 | -- resolve aliases
259 | params, err = canonicalize(params, self.spec)
260 |
261 | if not params then
262 | self:fail_json({msg="Err: " .. tostring(err)})
263 | end
264 |
265 | for k,v in pairs(self.spec) do
266 | -- setup defaults
267 | if v['default'] then
268 | if nil == params[k] then
269 | params[k] = v['default']
270 | end
271 | end
272 |
273 | -- assert requires
274 | if v['required'] then
275 | if not params[k] then
276 | self:fail_json({msg="Required parameter " .. k .. " not provided"})
277 | end
278 | end
279 | end
280 |
281 | -- check types/choices
282 | for k,v in pairs(params) do
283 | local typedesc = self.spec[k]['type']
284 | if typedesc then
285 | local val, err = check_transform_type(v, typedesc)
286 | if nil ~= val then
287 | params[k] = val
288 | else
289 | self:fail_json({msg="Err: " .. tostring(err)})
290 | end
291 | end
292 |
293 | local choices = self.spec[k]['choices']
294 | if choices then
295 | if not Ansible.contains(v, choices) then
296 | self:fail_json({msg=v .. " not a valid choice for " .. k})
297 | end
298 | end
299 | end
300 |
301 | self.params = params
302 |
303 | return params
304 | end
305 |
306 | local function file_exists(path)
307 | local f=io.open(path,"r")
308 | if f~=nil then
309 | io.close(f)
310 | return true
311 | else
312 | return false
313 | end
314 | end
315 |
316 | function Ansible:get_bin_path(name, required, candidates)
317 | if not candidates then
318 | candidates = {}
319 | end
320 |
321 | local path = os.getenv("PATH")
322 | if path then
323 | candidates = append(candidates, split(path, ":"))
324 | end
325 |
326 | for _,dir in pairs(candidates) do
327 | local fpath = dir .. "/" .. name
328 | if file_exists(fpath) then
329 | return fpath
330 | end
331 | end
332 |
333 | if required then
334 | self:fail_json({msg="No executable " .. name .. " found in PATH or candidates"})
335 | end
336 |
337 | return nil
338 | end
339 |
340 | function Ansible:remove_file(path)
341 | local rc, err = os.remove(path)
342 | if nil == rc then
343 | self:fail_json({msg="Internal, execute: failed to remove file " .. path})
344 | end
345 | return rc
346 | end
347 |
348 | local function get_version()
349 | local version = assert(string.match(_VERSION, "Lua (%d+.%d+)"))
350 | return tonumber(version) -- Aaaah, it hurts to use floating point like this...
351 | end
352 |
353 | function Ansible:run_command(command)
354 | local stdout = os.tmpname()
355 | local stderr = os.tmpname()
356 |
357 | local cmd = string.format("%s >%q 2>%q", command, stdout, stderr)
358 |
359 | local rc = nil
360 | if 5.1 < get_version() then
361 | _, _, rc = os.execute(cmd)
362 | else
363 | rc = os.execute(cmd)
364 | end
365 |
366 | local out = self:slurp(stdout)
367 | local err = self:slurp(stderr)
368 |
369 | self:remove_file(stdout)
370 | self:remove_file(stderr)
371 |
372 | return rc, out, err
373 | end
374 |
375 | function Ansible:copy(src, dest)
376 | local command = string.format("cp -f %q %q", src, dest)
377 | local rc, _, err = self:run_command(command)
378 |
379 | if rc ~= 0 then
380 | return false, err
381 | else
382 | return true, err
383 | end
384 | end
385 |
386 | function Ansible:move(src, dest)
387 | local command = string.format("mv -f %q %q", src, dest)
388 | local rc, _, err = self:run_command(command)
389 |
390 | if rc ~= 0 then
391 | return false, err
392 | else
393 | return true, err
394 | end
395 | end
396 |
397 | function Ansible:fail_json(kwargs)
398 | assert(kwargs['msg'])
399 | kwargs['failed'] = true
400 | if nil == kwargs['changed'] then
401 | kwargs['changed'] = false
402 | end
403 | if nil == kwargs['invocation'] then
404 | kwargs['invocations'] = {module_args=self.params}
405 | end
406 |
407 | io.write(json.encode(kwargs))
408 | os.exit(1)
409 | end
410 |
411 | function Ansible:exit_json(kwargs)
412 | if nil == kwargs['changed'] then
413 | kwargs['changed'] = false
414 | end
415 | if nil == kwargs['invocation'] then
416 | kwargs['invocations'] = {module_args=self:get_params()}
417 | end
418 |
419 | io.write(json.encode(kwargs))
420 | os.exit(0)
421 | end
422 |
423 | function Ansible:get_params()
424 | return self.params
425 | end
426 |
427 | function Ansible:ubus_connect()
428 | local p = self:get_params()
429 | local timeout = p['timeout']
430 | if not timeout then
431 | timeout = 30
432 | end
433 | local socket = p['socket']
434 |
435 | local conn = ubus.connect(socket, timeout)
436 | if not conn then
437 | self:fail_json({msg="Failed to connect to ubus"})
438 | end
439 |
440 | return conn
441 | end
442 |
443 | function Ansible:ubus_call(conn, namespace, procedure, arg)
444 | local res, status = conn:call(namespace, procedure, arg)
445 |
446 | if nil ~= status and 0 ~= status then
447 | self:fail_json({msg="Ubus call failed", call={namespace=namespace, procedure=procedure, arg=arg, status=status}})
448 | end
449 |
450 | return res
451 | end
452 |
453 | function Ansible:backup_local(file)
454 | local backupdest
455 |
456 | if file_exits(file) then
457 | local ext = os.time("%Y-%m-%d@H:%M:%S~")
458 |
459 | backupdest = string.format("%s.%s", file, ext)
460 |
461 | local content = self:slurp(file)
462 | local res = self:unslurp(backupdest, content)
463 | end
464 |
465 | return backupdest
466 | end
467 |
468 | function Ansible:is_dir(path)
469 | local f, err, code = io.open(path, "r")
470 |
471 | if nil == f then
472 | return false, err, code
473 | end
474 |
475 | local ok, err, code = f:read(1)
476 | f:close()
477 | return code == 21, nil, nil
478 | end
479 |
480 | function Ansible:check_mode()
481 | return self.internal_params["_ansible_check_mode"]
482 | end
483 |
484 | return Ansible
485 |
--------------------------------------------------------------------------------
/src/ansible_test.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | local Ansible = require("ansible")
4 |
5 | function main(arg)
6 | local module = Ansible.new({
7 | required = { required=true },
8 | choice = { choices={"a", "b", "c", "d"} },
9 | alias = { aliases={"al", "alia"}},
10 | default = { default="a" },
11 | -- Types str list dict bool int float path raw jsonarg
12 | string = { type='str' },
13 | list = { type='list' },
14 | bool = { type='bool' },
15 | int = { type='int' },
16 | float = { type='float' },
17 | dict = { type='dict' },
18 | path = { type='path' },
19 | raw = { type='raw' },
20 | jsonarg = { type='jsonarg' },
21 | reqalias = { aliases={"ra"}, required=true },
22 | defchoice = { default="foo", choices={"foo", "bar", "baz"}},
23 | defreq = { default="bar", required=true },
24 | change = { type='bool' },
25 | command = {},
26 | binpath = { type='dict' }
27 | })
28 |
29 | module:parse(arg[1])
30 |
31 | local p = module:get_params();
32 |
33 | if p["command"] then
34 | local rc, out, err = module:run_command(p["command"])
35 | if 0 == rc then
36 | module:exit_json({msg="Success", rc=rc, out=out, err=err})
37 | else
38 | module:fail_json({msg="Failure", rc=rc, out=out, err=err})
39 | end
40 | elseif p["binpath"] then
41 | local binspec = p["binpath"]
42 | local binpath = module:get_bin_path(binspec["name"], binspec["required"], binspec["candidates"])
43 | module:exit_json({msg="This is binpath", binpath=binpath})
44 | elseif p["change"] then
45 | module:exit_json({msg="This is an echo", changed=true})
46 | else
47 | module:exit_json({msg="This is an echo"})
48 | end
49 | end
50 |
51 | main(arg)
52 |
--------------------------------------------------------------------------------
/src/base64.lua:
--------------------------------------------------------------------------------
1 | -- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss ,
2 | -- 2018 Piotr Śliwka
3 | -- licensed under the terms of the LGPL2
4 |
5 | local base64 = {}
6 |
7 | -- character table string
8 | local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
9 |
10 | -- encoding
11 | function base64.encode(data)
12 | return ((data:gsub('.', function(x)
13 | local r,b='',x:byte()
14 | for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
15 | return r;
16 | end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
17 | if (#x < 6) then return '' end
18 | local c=0
19 | for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
20 | return b:sub(c+1,c+1)
21 | end)..({ '', '==', '=' })[#data%3+1])
22 | end
23 |
24 | -- decoding
25 | function base64.decode(data)
26 | data = string.gsub(data, '[^'..b..'=]', '')
27 | return (data:gsub('.', function(x)
28 | if (x == '=') then return '' end
29 | local r,f='',(b:find(x)-1)
30 | for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
31 | return r;
32 | end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
33 | if (#x ~= 8) then return '' end
34 | local c=0
35 | for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
36 | return string.char(c)
37 | end))
38 | end
39 |
40 | return base64
41 |
--------------------------------------------------------------------------------
/src/copy.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local File = require("fileutils")
5 | local os = require("os")
6 |
7 |
8 | function adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, index, module, directory_args, changed)
9 | -- Walk the new directories list and make sure that permissions are as we would expect
10 |
11 | local changed = false
12 |
13 | if index <= #new_directory_list then
14 | local working_dir = File.join(pre_existing_dir, new_directory_list[i])
15 | directory_args['path'] = working_dir
16 | changed = File.set_fs_attributes_if_different(module, directory_args, changed, nil)
17 | changed = adjust_recursive_directory_permissions(working_dir, new_directory_list, index+1, module, directory_args, changed)
18 | end
19 |
20 | return changed
21 | end
22 |
23 | function main(arg)
24 | local module = Ansible.new(
25 | { src = { required=true }
26 | , original_basename = { aliases={"_original_basename"}, required=false }
27 | , content = { required=false }
28 | , path = { aliases={'dest'}, required=true }
29 | , backup = { default=false, type='bool' }
30 | , force = { default=true, aliases={'thirsty'}, type='bool' }
31 | , validate = { required=false, type='str' }
32 | , directory_mode = { required=false }
33 | , remote_src = { required=false, type='bool' }
34 | -- sha256sum, to check if the copy was successful - currently ignored
35 | , checksum = {}
36 |
37 |
38 | -- file common args
39 | -- , src = {}
40 | , mode = { type='raw' }
41 | , owner = {}
42 | , group = {}
43 |
44 | -- Selinux to ignore
45 | , seuser = {}
46 | , serole = {}
47 | , selevel = {}
48 | , setype = {}
49 |
50 | , follow = {type='bool', default=false}
51 |
52 | -- not taken by the file module, but other modules call file so it must ignore them
53 | , content = {}
54 | , backup = {}
55 | -- , force = {}
56 | , remote_src = {}
57 | , regexp = {}
58 | , delimiter = {}
59 | -- , directory_mode = {}
60 | }
61 | )
62 |
63 | module:parse(arg[1])
64 |
65 | local p = module:get_params()
66 |
67 | local src = File.expanduser(p['src'])
68 | local dest = File.expanduser(p['path'])
69 | local backup = p['backup']
70 | local force = p['force']
71 | local _original_basename = p['_original_basename']
72 | local validate = p['validate']
73 | local follow = p['follow']
74 | local mode = p['mode']
75 | local remote_src = p['remote_src']
76 |
77 | if not File.exists(src) then
78 | module:fail_json({msg="Source " .. src .. " not found"})
79 | end
80 | if not File.readable(src) then
81 | module:fail_json({msg="Source " .. src .. " not readable"})
82 | end
83 | if File.isdir(src) then
84 | module:fail_json({msg="Remote copy does not support recursive copy of directory: " .. src})
85 | end
86 |
87 | local checksum_src = File.sha1(module, src)
88 | local checksum_dest = nil
89 | local md5sum_src = File.md5(module, src)
90 |
91 | local changed = false
92 |
93 | -- Special handling for recursive copy - create intermediate dirs
94 | if _original_basename and string.match(dest, "/$") then
95 | dest = File.join(dest, orignal_basename)
96 | local dirname = File.dirname(dest)
97 | if not File.exists(dirname) and File.isabs(dirname) then
98 | local pre_existing_dir, new_directory_list = File.split_pre_existing_dir(dirname)
99 | File.mkdirs(dirname)
100 | local directory_args = p
101 | local direcotry_mode = p['directory_mode']
102 | adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, 1, module, directory_args, changed)
103 | end
104 | end
105 |
106 | if File.exists(dest) then
107 | if File.islnk(dest) and follow then
108 | dest = File.realpath(dest)
109 | end
110 | if not force then
111 | module:exit_json({msg="file already exists", src=src, dest=dest, changed=false})
112 | end
113 | if File.isdir(dest) then
114 | local basename = File.basename(src)
115 | if _original_basename then
116 | basename = _original_basename
117 | end
118 | dest = File.join(dest, basename)
119 | end
120 | if File.readable(dest) then
121 | checksum_dest = File.sha1(module, dest)
122 | end
123 | else
124 | if not File.exists(File.dirname(dest)) then
125 | if nil == File.stat(File.dirname(dest)) then
126 | module:fail_json({msg="Destination directory " .. File.dirname(dest) .. " is not accessible"})
127 | end
128 | module:fail_json({msg="Destination directory " .. File.dirname(dest) .. " does not exist"})
129 | end
130 | end
131 |
132 | if not File.writeable(File.dirname(dest)) then
133 | module:fail_json({msg="Destination " .. File.dirname(dest) .. " not writeable"})
134 | end
135 |
136 | local backup_file = nil
137 | if checksum_src ~= checksum_dest or File.islnk(dest) then
138 | if not module:check_mode() then
139 | if backup and File.exists(dest) then
140 | backup_file = module:backup_local(dest)
141 | end
142 |
143 | local function err(res, msg)
144 | if not res then
145 | module:fail_json({msg="failed to copy: " .. src .. " to " .. dest .. ": " .. msg})
146 | end
147 | end
148 |
149 | local res, msg
150 | -- allow for conversion from symlink
151 | if File.islnk(dest) then
152 | res, msg = File.unlink(dest)
153 | err(res, msg)
154 | res, msg = File.touch(dest)
155 | err(res, msg)
156 | end
157 | if validate then
158 | -- FIXME: Validate is currently unsupported
159 | end
160 | if remote_src then
161 | local tmpname, msg = File.mkstemp(File.dirname(dest) .. "/ansibltmp_XXXXXX")
162 | err(tmpname, msg)
163 | res, msg = module:copy(src, tmpdest)
164 | err(res, msg)
165 | res, msg = module:move(tmpdest, dest)
166 | err(res, msg)
167 | else
168 | res, msg = module:move(src, dest)
169 | err(res, msg)
170 | end
171 | end
172 | changed = true
173 | else
174 | changed = false
175 | end
176 |
177 | res_args = { dest=dest, src=src, md5sum=md5sum_src, checksum=checksum_src, changed=changed }
178 | if backup_file then
179 | res_args['backup_file'] = backup_file
180 | end
181 |
182 | p['dest'] = dest
183 | if not module:check_mode() then
184 | local file_args = p
185 | res_args['changed'] = File.set_fs_attributes_if_different(module, file_args, res_args['changed'], nil)
186 | end
187 |
188 | res_args['msg'] = "Dummy"
189 |
190 | module:exit_json(res_args)
191 | end
192 |
193 | main(arg)
194 |
--------------------------------------------------------------------------------
/src/dkjson.lua:
--------------------------------------------------------------------------------
1 | -- Module options:
2 | local always_try_using_lpeg = true
3 | local register_global_module_table = false
4 | local global_module_name = 'json'
5 |
6 | --[==[
7 |
8 | David Kolf's JSON module for Lua 5.1/5.2
9 |
10 | Version 2.5
11 |
12 |
13 | For the documentation see the corresponding readme.txt or visit
14 | .
15 |
16 | You can contact the author by sending an e-mail to 'david' at the
17 | domain 'dkolf.de'.
18 |
19 |
20 | Copyright (C) 2010-2013 David Heiko Kolf
21 |
22 | Permission is hereby granted, free of charge, to any person obtaining
23 | a copy of this software and associated documentation files (the
24 | "Software"), to deal in the Software without restriction, including
25 | without limitation the rights to use, copy, modify, merge, publish,
26 | distribute, sublicense, and/or sell copies of the Software, and to
27 | permit persons to whom the Software is furnished to do so, subject to
28 | the following conditions:
29 |
30 | The above copyright notice and this permission notice shall be
31 | included in all copies or substantial portions of the Software.
32 |
33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
37 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
38 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
39 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40 | SOFTWARE.
41 |
42 | --]==]
43 |
44 | -- global dependencies:
45 | local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
46 | pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
47 | local error, require, pcall, select = error, require, pcall, select
48 | local floor, huge = math.floor, math.huge
49 | local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
50 | string.rep, string.gsub, string.sub, string.byte, string.char,
51 | string.find, string.len, string.format
52 | local strmatch = string.match
53 | local concat = table.concat
54 |
55 | local json = { version = "dkjson 2.5" }
56 |
57 | if register_global_module_table then
58 | _G[global_module_name] = json
59 | end
60 |
61 | local _ENV = nil -- blocking globals in Lua 5.2
62 |
63 | pcall (function()
64 | -- Enable access to blocked metatables.
65 | -- Don't worry, this module doesn't change anything in them.
66 | local debmeta = require "debug".getmetatable
67 | if debmeta then getmetatable = debmeta end
68 | end)
69 |
70 | json.null = setmetatable ({}, {
71 | __tojson = function () return "null" end
72 | })
73 |
74 | local function isarray (tbl)
75 | local max, n, arraylen = 0, 0, 0
76 | for k,v in pairs (tbl) do
77 | if k == 'n' and type(v) == 'number' then
78 | arraylen = v
79 | if v > max then
80 | max = v
81 | end
82 | else
83 | if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
84 | return false
85 | end
86 | if k > max then
87 | max = k
88 | end
89 | n = n + 1
90 | end
91 | end
92 | if max > 10 and max > arraylen and max > n * 2 then
93 | return false -- don't create an array with too many holes
94 | end
95 | return true, max
96 | end
97 |
98 | local escapecodes = {
99 | ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
100 | ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
101 | }
102 |
103 | local function escapeutf8 (uchar)
104 | local value = escapecodes[uchar]
105 | if value then
106 | return value
107 | end
108 | local a, b, c, d = strbyte (uchar, 1, 4)
109 | a, b, c, d = a or 0, b or 0, c or 0, d or 0
110 | if a <= 0x7f then
111 | value = a
112 | elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
113 | value = (a - 0xc0) * 0x40 + b - 0x80
114 | elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
115 | value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
116 | elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
117 | value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
118 | else
119 | return ""
120 | end
121 | if value <= 0xffff then
122 | return strformat ("\\u%.4x", value)
123 | elseif value <= 0x10ffff then
124 | -- encode as UTF-16 surrogate pair
125 | value = value - 0x10000
126 | local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
127 | return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
128 | else
129 | return ""
130 | end
131 | end
132 |
133 | local function fsub (str, pattern, repl)
134 | -- gsub always builds a new string in a buffer, even when no match
135 | -- exists. First using find should be more efficient when most strings
136 | -- don't contain the pattern.
137 | if strfind (str, pattern) then
138 | return gsub (str, pattern, repl)
139 | else
140 | return str
141 | end
142 | end
143 |
144 | local function quotestring (value)
145 | -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
146 | value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
147 | if strfind (value, "[\194\216\220\225\226\239]") then
148 | value = fsub (value, "\194[\128-\159\173]", escapeutf8)
149 | value = fsub (value, "\216[\128-\132]", escapeutf8)
150 | value = fsub (value, "\220\143", escapeutf8)
151 | value = fsub (value, "\225\158[\180\181]", escapeutf8)
152 | value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
153 | value = fsub (value, "\226\129[\160-\175]", escapeutf8)
154 | value = fsub (value, "\239\187\191", escapeutf8)
155 | value = fsub (value, "\239\191[\176-\191]", escapeutf8)
156 | end
157 | return "\"" .. value .. "\""
158 | end
159 | json.quotestring = quotestring
160 |
161 | local function replace(str, o, n)
162 | local i, j = strfind (str, o, 1, true)
163 | if i then
164 | return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
165 | else
166 | return str
167 | end
168 | end
169 |
170 | -- locale independent num2str and str2num functions
171 | local decpoint, numfilter
172 |
173 | local function updatedecpoint ()
174 | decpoint = strmatch(tostring(0.5), "([^05+])")
175 | -- build a filter that can be used to remove group separators
176 | numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
177 | end
178 |
179 | updatedecpoint()
180 |
181 | local function num2str (num)
182 | return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
183 | end
184 |
185 | local function str2num (str)
186 | local num = tonumber(replace(str, ".", decpoint))
187 | if not num then
188 | updatedecpoint()
189 | num = tonumber(replace(str, ".", decpoint))
190 | end
191 | return num
192 | end
193 |
194 | local function addnewline2 (level, buffer, buflen)
195 | buffer[buflen+1] = "\n"
196 | buffer[buflen+2] = strrep (" ", level)
197 | buflen = buflen + 2
198 | return buflen
199 | end
200 |
201 | function json.addnewline (state)
202 | if state.indent then
203 | state.bufferlen = addnewline2 (state.level or 0,
204 | state.buffer, state.bufferlen or #(state.buffer))
205 | end
206 | end
207 |
208 | local encode2 -- forward declaration
209 |
210 | local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
211 | local kt = type (key)
212 | if kt ~= 'string' and kt ~= 'number' then
213 | return nil, "type '" .. kt .. "' is not supported as a key by JSON."
214 | end
215 | if prev then
216 | buflen = buflen + 1
217 | buffer[buflen] = ","
218 | end
219 | if indent then
220 | buflen = addnewline2 (level, buffer, buflen)
221 | end
222 | buffer[buflen+1] = quotestring (key)
223 | buffer[buflen+2] = ":"
224 | return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
225 | end
226 |
227 | local function appendcustom(res, buffer, state)
228 | local buflen = state.bufferlen
229 | if type (res) == 'string' then
230 | buflen = buflen + 1
231 | buffer[buflen] = res
232 | end
233 | return buflen
234 | end
235 |
236 | local function exception(reason, value, state, buffer, buflen, defaultmessage)
237 | defaultmessage = defaultmessage or reason
238 | local handler = state.exception
239 | if not handler then
240 | return nil, defaultmessage
241 | else
242 | state.bufferlen = buflen
243 | local ret, msg = handler (reason, value, state, defaultmessage)
244 | if not ret then return nil, msg or defaultmessage end
245 | return appendcustom(ret, buffer, state)
246 | end
247 | end
248 |
249 | function json.encodeexception(reason, value, state, defaultmessage)
250 | return quotestring("<" .. defaultmessage .. ">")
251 | end
252 |
253 | encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
254 | local valtype = type (value)
255 | local valmeta = getmetatable (value)
256 | valmeta = type (valmeta) == 'table' and valmeta -- only tables
257 | local valtojson = valmeta and valmeta.__tojson
258 | if valtojson then
259 | if tables[value] then
260 | return exception('reference cycle', value, state, buffer, buflen)
261 | end
262 | tables[value] = true
263 | state.bufferlen = buflen
264 | local ret, msg = valtojson (value, state)
265 | if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
266 | tables[value] = nil
267 | buflen = appendcustom(ret, buffer, state)
268 | elseif value == nil then
269 | buflen = buflen + 1
270 | buffer[buflen] = "null"
271 | elseif valtype == 'number' then
272 | local s
273 | if value ~= value or value >= huge or -value >= huge then
274 | -- This is the behaviour of the original JSON implementation.
275 | s = "null"
276 | else
277 | s = num2str (value)
278 | end
279 | buflen = buflen + 1
280 | buffer[buflen] = s
281 | elseif valtype == 'boolean' then
282 | buflen = buflen + 1
283 | buffer[buflen] = value and "true" or "false"
284 | elseif valtype == 'string' then
285 | buflen = buflen + 1
286 | buffer[buflen] = quotestring (value)
287 | elseif valtype == 'table' then
288 | if tables[value] then
289 | return exception('reference cycle', value, state, buffer, buflen)
290 | end
291 | tables[value] = true
292 | level = level + 1
293 | local isa, n = isarray (value)
294 | if n == 0 and valmeta and valmeta.__jsontype == 'object' then
295 | isa = false
296 | end
297 | local msg
298 | if isa then -- JSON array
299 | buflen = buflen + 1
300 | buffer[buflen] = "["
301 | for i = 1, n do
302 | buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
303 | if not buflen then return nil, msg end
304 | if i < n then
305 | buflen = buflen + 1
306 | buffer[buflen] = ","
307 | end
308 | end
309 | buflen = buflen + 1
310 | buffer[buflen] = "]"
311 | else -- JSON object
312 | local prev = false
313 | buflen = buflen + 1
314 | buffer[buflen] = "{"
315 | local order = valmeta and valmeta.__jsonorder or globalorder
316 | if order then
317 | local used = {}
318 | n = #order
319 | for i = 1, n do
320 | local k = order[i]
321 | local v = value[k]
322 | if v then
323 | used[k] = true
324 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
325 | prev = true -- add a seperator before the next element
326 | end
327 | end
328 | for k,v in pairs (value) do
329 | if not used[k] then
330 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
331 | if not buflen then return nil, msg end
332 | prev = true -- add a seperator before the next element
333 | end
334 | end
335 | else -- unordered
336 | for k,v in pairs (value) do
337 | buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
338 | if not buflen then return nil, msg end
339 | prev = true -- add a seperator before the next element
340 | end
341 | end
342 | if indent then
343 | buflen = addnewline2 (level - 1, buffer, buflen)
344 | end
345 | buflen = buflen + 1
346 | buffer[buflen] = "}"
347 | end
348 | tables[value] = nil
349 | else
350 | return exception ('unsupported type', value, state, buffer, buflen,
351 | "type '" .. valtype .. "' is not supported by JSON.")
352 | end
353 | return buflen
354 | end
355 |
356 | function json.encode (value, state)
357 | state = state or {}
358 | local oldbuffer = state.buffer
359 | local buffer = oldbuffer or {}
360 | state.buffer = buffer
361 | updatedecpoint()
362 | local ret, msg = encode2 (value, state.indent, state.level or 0,
363 | buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
364 | if not ret then
365 | error (msg, 2)
366 | elseif oldbuffer == buffer then
367 | state.bufferlen = ret
368 | return true
369 | else
370 | state.bufferlen = nil
371 | state.buffer = nil
372 | return concat (buffer)
373 | end
374 | end
375 |
376 | local function loc (str, where)
377 | local line, pos, linepos = 1, 1, 0
378 | while true do
379 | pos = strfind (str, "\n", pos, true)
380 | if pos and pos < where then
381 | line = line + 1
382 | linepos = pos
383 | pos = pos + 1
384 | else
385 | break
386 | end
387 | end
388 | return "line " .. line .. ", column " .. (where - linepos)
389 | end
390 |
391 | local function unterminated (str, what, where)
392 | return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
393 | end
394 |
395 | local function scanwhite (str, pos)
396 | while true do
397 | pos = strfind (str, "%S", pos)
398 | if not pos then return nil end
399 | local sub2 = strsub (str, pos, pos + 1)
400 | if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
401 | -- UTF-8 Byte Order Mark
402 | pos = pos + 3
403 | elseif sub2 == "//" then
404 | pos = strfind (str, "[\n\r]", pos + 2)
405 | if not pos then return nil end
406 | elseif sub2 == "/*" then
407 | pos = strfind (str, "*/", pos + 2)
408 | if not pos then return nil end
409 | pos = pos + 2
410 | else
411 | return pos
412 | end
413 | end
414 | end
415 |
416 | local escapechars = {
417 | ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
418 | ["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
419 | }
420 |
421 | local function unichar (value)
422 | if value < 0 then
423 | return nil
424 | elseif value <= 0x007f then
425 | return strchar (value)
426 | elseif value <= 0x07ff then
427 | return strchar (0xc0 + floor(value/0x40),
428 | 0x80 + (floor(value) % 0x40))
429 | elseif value <= 0xffff then
430 | return strchar (0xe0 + floor(value/0x1000),
431 | 0x80 + (floor(value/0x40) % 0x40),
432 | 0x80 + (floor(value) % 0x40))
433 | elseif value <= 0x10ffff then
434 | return strchar (0xf0 + floor(value/0x40000),
435 | 0x80 + (floor(value/0x1000) % 0x40),
436 | 0x80 + (floor(value/0x40) % 0x40),
437 | 0x80 + (floor(value) % 0x40))
438 | else
439 | return nil
440 | end
441 | end
442 |
443 | local function scanstring (str, pos)
444 | local lastpos = pos + 1
445 | local buffer, n = {}, 0
446 | while true do
447 | local nextpos = strfind (str, "[\"\\]", lastpos)
448 | if not nextpos then
449 | return unterminated (str, "string", pos)
450 | end
451 | if nextpos > lastpos then
452 | n = n + 1
453 | buffer[n] = strsub (str, lastpos, nextpos - 1)
454 | end
455 | if strsub (str, nextpos, nextpos) == "\"" then
456 | lastpos = nextpos + 1
457 | break
458 | else
459 | local escchar = strsub (str, nextpos + 1, nextpos + 1)
460 | local value
461 | if escchar == "u" then
462 | value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
463 | if value then
464 | local value2
465 | if 0xD800 <= value and value <= 0xDBff then
466 | -- we have the high surrogate of UTF-16. Check if there is a
467 | -- low surrogate escaped nearby to combine them.
468 | if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
469 | value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
470 | if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
471 | value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
472 | else
473 | value2 = nil -- in case it was out of range for a low surrogate
474 | end
475 | end
476 | end
477 | value = value and unichar (value)
478 | if value then
479 | if value2 then
480 | lastpos = nextpos + 12
481 | else
482 | lastpos = nextpos + 6
483 | end
484 | end
485 | end
486 | end
487 | if not value then
488 | value = escapechars[escchar] or escchar
489 | lastpos = nextpos + 2
490 | end
491 | n = n + 1
492 | buffer[n] = value
493 | end
494 | end
495 | if n == 1 then
496 | return buffer[1], lastpos
497 | elseif n > 1 then
498 | return concat (buffer), lastpos
499 | else
500 | return "", lastpos
501 | end
502 | end
503 |
504 | local scanvalue -- forward declaration
505 |
506 | local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
507 | local len = strlen (str)
508 | local tbl, n = {}, 0
509 | local pos = startpos + 1
510 | if what == 'object' then
511 | setmetatable (tbl, objectmeta)
512 | else
513 | setmetatable (tbl, arraymeta)
514 | end
515 | while true do
516 | pos = scanwhite (str, pos)
517 | if not pos then return unterminated (str, what, startpos) end
518 | local char = strsub (str, pos, pos)
519 | if char == closechar then
520 | return tbl, pos + 1
521 | end
522 | local val1, err
523 | val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
524 | if err then return nil, pos, err end
525 | pos = scanwhite (str, pos)
526 | if not pos then return unterminated (str, what, startpos) end
527 | char = strsub (str, pos, pos)
528 | if char == ":" then
529 | if val1 == nil then
530 | return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
531 | end
532 | pos = scanwhite (str, pos + 1)
533 | if not pos then return unterminated (str, what, startpos) end
534 | local val2
535 | val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
536 | if err then return nil, pos, err end
537 | tbl[val1] = val2
538 | pos = scanwhite (str, pos)
539 | if not pos then return unterminated (str, what, startpos) end
540 | char = strsub (str, pos, pos)
541 | else
542 | n = n + 1
543 | tbl[n] = val1
544 | end
545 | if char == "," then
546 | pos = pos + 1
547 | end
548 | end
549 | end
550 |
551 | scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
552 | pos = pos or 1
553 | pos = scanwhite (str, pos)
554 | if not pos then
555 | return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
556 | end
557 | local char = strsub (str, pos, pos)
558 | if char == "{" then
559 | return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
560 | elseif char == "[" then
561 | return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
562 | elseif char == "\"" then
563 | return scanstring (str, pos)
564 | else
565 | local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
566 | if pstart then
567 | local number = str2num (strsub (str, pstart, pend))
568 | if number then
569 | return number, pend + 1
570 | end
571 | end
572 | pstart, pend = strfind (str, "^%a%w*", pos)
573 | if pstart then
574 | local name = strsub (str, pstart, pend)
575 | if name == "true" then
576 | return true, pend + 1
577 | elseif name == "false" then
578 | return false, pend + 1
579 | elseif name == "null" then
580 | return nullval, pend + 1
581 | end
582 | end
583 | return nil, pos, "no valid JSON value at " .. loc (str, pos)
584 | end
585 | end
586 |
587 | local function optionalmetatables(...)
588 | if select("#", ...) > 0 then
589 | return ...
590 | else
591 | return {__jsontype = 'object'}, {__jsontype = 'array'}
592 | end
593 | end
594 |
595 | function json.decode (str, pos, nullval, ...)
596 | local objectmeta, arraymeta = optionalmetatables(...)
597 | return scanvalue (str, pos, nullval, objectmeta, arraymeta)
598 | end
599 |
600 | function json.use_lpeg ()
601 | local g = require ("lpeg")
602 |
603 | if g.version() == "0.11" then
604 | error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
605 | end
606 |
607 | local pegmatch = g.match
608 | local P, S, R = g.P, g.S, g.R
609 |
610 | local function ErrorCall (str, pos, msg, state)
611 | if not state.msg then
612 | state.msg = msg .. " at " .. loc (str, pos)
613 | state.pos = pos
614 | end
615 | return false
616 | end
617 |
618 | local function Err (msg)
619 | return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
620 | end
621 |
622 | local SingleLineComment = P"//" * (1 - S"\n\r")^0
623 | local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
624 | local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
625 |
626 | local PlainChar = 1 - S"\"\\\n\r"
627 | local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
628 | local HexDigit = R("09", "af", "AF")
629 | local function UTF16Surrogate (match, pos, high, low)
630 | high, low = tonumber (high, 16), tonumber (low, 16)
631 | if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
632 | return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
633 | else
634 | return false
635 | end
636 | end
637 | local function UTF16BMP (hex)
638 | return unichar (tonumber (hex, 16))
639 | end
640 | local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
641 | local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
642 | local Char = UnicodeEscape + EscapeSequence + PlainChar
643 | local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
644 | local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
645 | local Fractal = P"." * R"09"^0
646 | local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
647 | local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
648 | local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
649 | local SimpleValue = Number + String + Constant
650 | local ArrayContent, ObjectContent
651 |
652 | -- The functions parsearray and parseobject parse only a single value/pair
653 | -- at a time and store them directly to avoid hitting the LPeg limits.
654 | local function parsearray (str, pos, nullval, state)
655 | local obj, cont
656 | local npos
657 | local t, nt = {}, 0
658 | repeat
659 | obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
660 | if not npos then break end
661 | pos = npos
662 | nt = nt + 1
663 | t[nt] = obj
664 | until cont == 'last'
665 | return pos, setmetatable (t, state.arraymeta)
666 | end
667 |
668 | local function parseobject (str, pos, nullval, state)
669 | local obj, key, cont
670 | local npos
671 | local t = {}
672 | repeat
673 | key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
674 | if not npos then break end
675 | pos = npos
676 | t[key] = obj
677 | until cont == 'last'
678 | return pos, setmetatable (t, state.objectmeta)
679 | end
680 |
681 | local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
682 | local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
683 | local Value = Space * (Array + Object + SimpleValue)
684 | local ExpectedValue = Value + Space * Err "value expected"
685 | ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
686 | local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
687 | ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
688 | local DecodeValue = ExpectedValue * g.Cp ()
689 |
690 | function json.decode (str, pos, nullval, ...)
691 | local state = {}
692 | state.objectmeta, state.arraymeta = optionalmetatables(...)
693 | local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
694 | if state.msg then
695 | return nil, state.pos, state.msg
696 | else
697 | return obj, retpos
698 | end
699 | end
700 |
701 | -- use this function only once:
702 | json.use_lpeg = function () return json end
703 |
704 | json.using_lpeg = true
705 |
706 | return json -- so you can get the module using json = require "dkjson".use_lpeg()
707 | end
708 |
709 | if always_try_using_lpeg then
710 | pcall (json.use_lpeg)
711 | end
712 |
713 | return json
714 |
715 |
--------------------------------------------------------------------------------
/src/fatpack.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use Getopt::Long;
7 | use File::Basename qw/dirname basename/;
8 | use Storable qw/dclone/;
9 | use Data::Compare;
10 | use Data::Dumper;
11 |
12 | my $outdir;
13 | my $truncate = '';
14 | my $inputfile;
15 | my $usage='';
16 | my @whitelist=('io');
17 |
18 | GetOptions('output=s' => \$outdir, 'truncate' => \$truncate, 'input=s' => \$inputfile, 'help' => \$usage, 'whitelist=s' => \@whitelist);
19 |
20 | @whitelist = uniq(split(/,/, join(',', @whitelist)));
21 | my $whitelisted = join '|', map{ "^" . $_ } map{quotemeta} sort {length($b)<=>length($a)}@whitelist;
22 | my $whitelistre = qr/($whitelisted)/;
23 |
24 | if ( $usage
25 | || ((!$outdir) || (! -d $outdir))
26 | || ((!$inputfile) || (! -f $inputfile))) {
27 | print <<"EOF";
28 | $0 --input --output [--truncate] [--whitelist ,]
29 | --help Print this help message
30 | --input file to fatpack. Expects all libs to reside in basedir(file)
31 | --output output directory for fatpacked files
32 | --truncate unconditionally override in outdir (default=false)
33 | --whitelist modules not to fatpack
34 | EOF
35 | exit -1;
36 | }
37 |
38 | sub uniq {
39 | my %seen;
40 | grep !$seen{$_}++, @_;
41 | }
42 |
43 | sub slurp {
44 | my $filename = shift;
45 | return do {
46 | local $/;
47 | open my $file, '<:encoding(UTF-8)', $filename or die "Failed to open file $filename";
48 | <$file>;
49 | };
50 | }
51 |
52 | sub extractIncludes {
53 | # get all requires from a given modules
54 | my $filecontent = shift;
55 |
56 | my @requires = ($filecontent =~ m/require\((.*?)\)/g);
57 | @requires = map {sanitizeRequire($_)} @requires;
58 |
59 | return \@requires;
60 | }
61 |
62 | sub sanitizeRequire {
63 | my $require = shift;
64 | # remove all quotes and whitespaces from requires
65 | $require =~ s/^['"\s]+|['"\s]+$//g;
66 | return $require;
67 | }
68 |
69 | sub fix {
70 | my $op = shift;
71 | my $old = shift;
72 | my $new = $old;
73 |
74 | do {
75 | $old = $new;
76 | $new = dclone($old);
77 | $new = $op->($new);
78 | } while (!Compare($old, $new));
79 |
80 | return $new;
81 | }
82 |
83 | sub getModule {
84 | my $includedir = shift;
85 | my $modulename = shift;
86 |
87 | return slurp("$includedir/$modulename.lua");
88 | }
89 |
90 | sub fatpack {
91 | my $mainmodule = shift;
92 | my $modules = shift;
93 |
94 | # split the module in shebang+header and the actual code
95 | $modules->{$mainmodule} =~ /^(?(#!.*\n|--.*\n)+)(?(.|\n)*)/;
96 |
97 | my $head = $+{head};
98 | my $tail = $+{tail};
99 |
100 | # Build the fatpacked script
101 | my $packed = $head;
102 |
103 | $packed .= <<"EOF";
104 |
105 | do
106 | local _ENV = _ENV
107 | EOF
108 |
109 | foreach my $module (sort keys %{$modules}) {
110 | next if ($module eq $mainmodule);
111 |
112 | my $effcontent = %{$modules}{$module};
113 | # Strip the shebang
114 | $effcontent =~ s/^#!.*\n//g;
115 |
116 | $packed .= <<"EOF";
117 | package.preload["$module"] = function( ... )
118 | local arg = _G.arg;
119 | _ENV = _ENV;
120 |
121 | $effcontent
122 | end
123 | EOF
124 | }
125 |
126 | $packed .= "\nend\n";
127 |
128 | $packed .= $tail;
129 |
130 | return $packed;
131 | }
132 |
133 | sub unslurp {
134 | my $dst = shift;
135 | my $content = shift;
136 |
137 | if (-f $dst && !$truncate) {
138 | print STDERR "$dst already exists and --truncate was not specified\n";
139 | exit(1);
140 | }
141 |
142 | open(my $fh, '>', $dst) or die "Could not open file '$dst'";
143 | print $fh $content;
144 | close $fh;
145 | }
146 |
147 | sub main {
148 | my $includedir = dirname($inputfile);
149 |
150 | my $inputmodule = basename($inputfile, ".lua");
151 |
152 | my $modules = {
153 | $inputmodule => getModule($includedir, $inputmodule),
154 | };
155 |
156 | my $process = sub {
157 | my $modules = shift;
158 |
159 | my @new = ();
160 |
161 | # gather all includes in all modules
162 | while(my ($key, $value) = each %{$modules}) {
163 | my $extracted = extractIncludes($value);
164 | push @new, @$extracted;
165 | }
166 |
167 | # Add all new modules and their contents
168 | foreach my $module (@new) {
169 | if (! exists $modules->{$module} && $module !~ $whitelistre) {
170 | $modules->{$module} = getModule($includedir, $module);
171 | }
172 | }
173 |
174 | return $modules;
175 | };
176 |
177 | $modules = fix($process, $modules);
178 |
179 | my $fat = fatpack($inputmodule, $modules);
180 |
181 | unslurp("$outdir/$inputmodule.lua", $fat);
182 | }
183 |
184 | main();
185 |
--------------------------------------------------------------------------------
/src/file.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local File = require("fileutils")
5 | local Errno = require("posix.errno")
6 | local unistd = require("posix.unistd")
7 | local time = require("posix.time")
8 |
9 | local function get_state(path)
10 | -- Find the current state
11 |
12 | if File.lexists(path) then
13 | local stat = File.stat(path)
14 | if File.islnk(path) then
15 | return 'link'
16 | elseif File.isdir(path) then
17 | return 'directory'
18 | elseif stat ~= nil and stat['st_nlink'] > 1 then
19 | return 'hard'
20 | else
21 | -- could be many other things but defaulting to file
22 | return 'file'
23 | end
24 | end
25 |
26 | return 'absent'
27 | end
28 |
29 | local function append(t1, t2)
30 | for k,v in ipairs(t2) do
31 | t1[#t1 + 1] = v
32 | end
33 | return t1
34 | end
35 |
36 | local function deepcopy(orig)
37 | local orig_type = type(orig)
38 | local copy
39 | if orig_type == 'table' then
40 | copy = {}
41 | for orig_key, orig_value in next, orig, nil do
42 | copy[deepcopy(orig_key)] = deepcopy(orig_value)
43 | end
44 | setmetatable(copy, deepcopy(getmetatable(orig)))
45 | else -- number, string, boolean, etc
46 | copy = orig
47 | end
48 | return copy
49 | end
50 |
51 | local function recursive_set_attributes(module, path, follow, file_args)
52 | local changed = false
53 | local out = {}
54 | for _, entry in ipairs(File.walk(path, false)) do
55 | local root = entry['root']
56 | local fsobjs = append(entry['dirs'], entry['files'])
57 |
58 | for _, fsobj in ipairs(fsobjs) do
59 | fsname = File.join(root, {fsobj})
60 | out[#out + 1] = fsname
61 |
62 | if not File.islnk(fsname) then
63 | local tmp_file_args = deepcopy(file_args)
64 | tmp_file_args['path'] = fsname
65 | changed = changed or File.set_fs_attributes_if_different(module, tmp_file_args, changed, nil)
66 | else
67 | local tmp_file_args = deepcopy(file_args)
68 | tmp_file_args['path'] = fsname
69 | changed = changed or File.set_fs_attributes_if_different(module, tmp_file_args, changed, nil)
70 | if follow then
71 | fsname = File.join(root, {File.readlink(fsname)})
72 | if File.isdir(fsname) then
73 | changed = changed or recursive_set_attributes(module, fsname, follow, file_args)
74 | end
75 | tmp_file_args = deepcopy(file_args)
76 | tmp_file_args['path'] = fsname
77 | changed = changed or File.set_fs_attributes_if_different(module, tmp_file_args, changed, nil)
78 | end
79 | end
80 | end
81 | end
82 |
83 | return changed
84 | end
85 |
86 | local function strip(str, chars)
87 | str = string.gsub(str, string.format("^[%s]+", chars), "")
88 | str = string.gsub(str, string.format("[%s]+$", chars), "")
89 | return str
90 | end
91 |
92 | local function lstrip(str, chars)
93 | return string.gsub(str, string.format("^[%s]+", chars), "")
94 | end
95 |
96 | local function rstrip(str, chars)
97 | return string.gsub(str, string.format("[%s]+$", chars), "")
98 | end
99 |
100 | local function split(str, delimiter)
101 | local toks = {}
102 |
103 | for tok in string.gmatch(str, "[^".. delimiter .. "]+") do
104 | toks[#toks + 1] = tok
105 | end
106 |
107 | return toks
108 | end
109 |
110 | function main(arg)
111 | local module = Ansible.new(
112 | { state = { choices={'file', 'directory', 'link', 'hard', 'touch', 'absent' } }
113 | , path = { aliases={'dest', 'name'}, required=true }
114 | , original_basename = { aliases={"_original_basename"}, required=false }
115 | , recurse = { default=false, type='bool' }
116 | , force = { required=false, default=false, type='bool' }
117 | , diff_peek = {}
118 | , validate = { required=false }
119 | , src = {required=false}
120 |
121 | -- file common args
122 | -- , src = {}
123 | , mode = { type='raw' }
124 | , owner = {}
125 | , group = {}
126 |
127 | -- Selinux to ignore
128 | , seuser = {}
129 | , serole = {}
130 | , selevel = {}
131 | , setype = {}
132 |
133 | , follow = {type='bool', default=false}
134 |
135 | -- not taken by the file module, but other modules call file so it must ignore them
136 | , content = {}
137 | , backup = {}
138 | , force = {}
139 | , remote_src = {}
140 | , regexp = {}
141 | , delimiter = {}
142 | , directory_mode = {}
143 | }
144 | )
145 |
146 | module:parse(arg[1])
147 |
148 | -- FIXME: properly implement checkmode handling in module
149 | -- NB: This module is already capable of performing check_mode
150 | local checkmode = false
151 |
152 | local params = module:get_params()
153 |
154 | local state = params['state']
155 | local force = params['force']
156 | local diff_peek = params['diff_peek']
157 | local src = params['src']
158 | local follow = params['follow']
159 |
160 | -- modify source as we later reload and pass, specially relevant when used by other modules
161 | path = File.expanduser(params['path'])
162 | params['path'] = path
163 |
164 | -- short-circuit for diff_peek
165 | if nil ~= diff_peek then
166 | local appears_binary = false
167 |
168 | local f, err = io.open(path, "r")
169 | if f ~= nil then
170 | local content = f:read(8192)
171 | if Ansible.contains('\x00', content) then
172 | appears_binary = true
173 | end
174 | end
175 |
176 | module.exit_json({path=path, changed=False, msg="Dummy", appears_binary=appears_binary})
177 | end
178 |
179 | prev_state = get_state(path)
180 |
181 | -- state should default to file, but since that creates many conflicts
182 | -- default to 'current' when it exists
183 | if nil == state then
184 | if prev_state ~= 'absent' then
185 | state = prev_state
186 | else
187 | state = 'file'
188 | end
189 | end
190 |
191 | -- source is both the source of a symlink or an informational passing of the src for a template module
192 | -- or copy module, even if this module never uses it, it is needed to key off some things
193 | if src ~= nil then
194 | src = File.expanduser(src)
195 | else
196 | if 'link' == state or 'hard' == state then
197 | if follow and 'link' == state then
198 | -- use the current target of the link as the source
199 | src = File.realpath(path)
200 | else
201 | module:fail_json({msg='src and dest are required for creating links'})
202 | end
203 | end
204 | end
205 |
206 | -- _original_basename is used by other modules that depend on file
207 | if File.isdir(path) and ("link" ~= state and "absent" ~= state) then
208 | local basename = nil
209 | if params['_original_basename'] then
210 | basename = params['_original_basename']
211 | elseif src ~= nil then
212 | basename = File.basename(src)
213 | end
214 | if basename then
215 | path = File.join(path, {basename})
216 | params['path'] = path
217 | end
218 | end
219 |
220 | -- make sure the target path is a directory when we're doing a recursive operation
221 | local recurse = params['recurse']
222 | if recurse and state ~= 'directory' then
223 | module:fail_json({path=path, msg="recurse option requires state to be directory"})
224 | end
225 |
226 | -- File args are inlined...
227 | local changed = false
228 | local diff = { before = {path=path}
229 | , after = {path=path}}
230 |
231 | local state_change = false
232 | if prev_state ~= state then
233 | diff['before']['state'] = prev_state
234 | diff['after']['state'] = state
235 | state_change = true
236 | end
237 |
238 | if state == 'absent' then
239 | if state_change then
240 | if not check_mode then
241 | if prev_state == 'directory' then
242 | local err = File.rmtree(path, {ignore_errors=false})
243 | if err then
244 | module:fail_json({msg="rmtree failed"})
245 | end
246 | else
247 | local status, errstr, errno = File.unlink(path)
248 | if not status then
249 | module:fail_json({path=path, msg="unlinking failed: " .. errstr})
250 | end
251 | end
252 | end
253 | module:exit_json({path=path, changed=true, msg="dummy", diff=diff})
254 | else
255 | module:exit_json({path=path, changed=false, msg="dummy"})
256 | end
257 | elseif state == 'file' then
258 | if state_change then
259 | if follow and prev_state == 'link' then
260 | -- follow symlink and operate on original
261 | path = File.realpath(path)
262 | prev_state = get_state(path)
263 | path['path'] = path
264 | end
265 | end
266 |
267 | if prev_state ~= 'file' and prev_state ~= 'hard' then
268 | -- file is not absent and any other state is a conflict
269 | module:fail_json({path = path, msg=string.format("file (%s) is %s, cannot continue", path, prev_state)})
270 | end
271 |
272 | changed = File.set_fs_attributes_if_different(module, params, changed, diff)
273 | module:exit_json({path=path, changed=changed, msg="dummy", diff=diff})
274 | elseif state == 'directory' then
275 | if follow and prev_state == 'link' then
276 | path = File.realpath(path)
277 | prev_state = get_state(path)
278 | end
279 |
280 | if prev_state == 'absent' then
281 | if module:check_mode() then
282 | module:exit_json({changed=true, msg="dummy", diff=diff})
283 | end
284 | changed = true
285 | local curpath = ''
286 |
287 | -- Split the path so we can apply filesystem attributes recursively
288 | -- from the root (/) directory for absolute paths or the base path
289 | -- of a relative path. We can then walk the appropriate directory
290 | -- path to apply attributes.
291 |
292 | local segments = split(strip(path, '/'), '/')
293 | for _, dirname in ipairs(segments) do
294 | curpath = curpath .. '/' .. dirname
295 | -- remove lieading slash if we're creating a relative path
296 | if not File.isabs(path) then
297 | curpath = lstrip(curpath, "/")
298 | end
299 | if not File.exists(curpath) then
300 | local status, errstr, errno = File.mkdir(path)
301 | if not status then
302 | if not (errno == Errno.EEXIST and File.isdir(curpath)) then
303 | module:fail_json({path=path, msg="There was an issue creating " .. curpath .. " as requested: " .. errstr})
304 | end
305 | end
306 | tmp_file_args = deepcopy(params)
307 | tmp_file_args['path'] = curpath
308 | changed = File.set_fs_attributes_if_different(module, params, changed, diff)
309 | end
310 | end
311 | elseif prev_state ~= 'directory' then
312 | module:fail_json({path=path, msg=path .. "already exists as a " .. prev_state})
313 | end
314 |
315 | changed = File.set_fs_attributes_if_different(module, params, changed, diff)
316 |
317 | if recurse then
318 | changed = changed or recursive_set_attributes(module, params['path'], follow, params)
319 | end
320 |
321 | module:exit_json({path=path, changed=changed, diff=diff, msg="Dummy"})
322 |
323 | elseif state == 'link' or state == 'hard' then
324 | local relpath
325 | if File.isdir(path) and not File.islnk(path) then
326 | relpath = path
327 | else
328 | relpath = File.dirname(path)
329 | end
330 |
331 | local absrc = File.join(relpath, {src})
332 | if not File.exists(absrc) and not force then
333 | module:fail_json({path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link ' .. absrc})
334 | end
335 |
336 | if state == 'hard' then
337 | if not File.isabs(src) then
338 | module:fail_json({msg="absolute paths are required"})
339 | end
340 | elseif pref_state == 'directory' then
341 | if not force then
342 | module:fail_json({path=path, msg="refusing to convert between " .. prev_state .. " and " .. state .. " for " .. path})
343 | else
344 | local lsdir = File.listdir(path)
345 | if lsdir and #lsdir > 0 then
346 | -- refuse to replace a directory that has files in it
347 | module:fail_json({path=path, msg="the directory " .. path .. " is not empty, refusing to convert it"})
348 | end
349 | end
350 | elseif (prev_state == "file" or prev_state == "hard") and not force then
351 | module:fail_json({path=path, msg="refusing to convert between " .. prev_state .. " and " .. state .. " for " .. path})
352 | end
353 |
354 | if prev_state == 'absent' then
355 | changed = true
356 | elseif prev_state == 'link' then
357 | local old_src = File.readlink(path)
358 | if old_src ~= src then
359 | changed = true
360 | end
361 | elseif prev_state == 'hard' then
362 | if not (state == 'hard' and File.stat(path)['st_ino'] == File.stat(src)['st_ino']) then
363 | changed = true
364 | if not force then
365 | module:fail_json({dest=path, src=src, msg='Cannot link, different hard link exists at destination'})
366 | end
367 | end
368 | elseif prev_state == 'file' or prev_state == 'directory' then
369 | changed = true
370 | if not force then
371 | module:fail_json({dest=path, src=src, msg='Cannot link, ' .. prev_state .. ' exists at destination'})
372 | end
373 | else
374 | module:fail_json({dest=path, src=src, msg='unexpected position reached'})
375 | end
376 |
377 | if changed and not module:check_mode() then
378 | if prev_state ~= absent then
379 | -- try to replace automically
380 | local tmppath = string.format("%s/.%d.%d.tmp", File.dirname(path), unistd.getpid(), time.time())
381 |
382 | local status, errstr, errno
383 | if prev_state == 'directory' and (state == 'hard' or state == 'link')then
384 | status, errstr, errno = File.rmdir(path)
385 | end
386 | if state == 'hard' then
387 | status, errstr, errno = File.link(src, tmppath)
388 | else
389 | status, errstr, errno = File.symlink(src, tmppath)
390 | end
391 | if status then
392 | status, errstr, errno = File.rename(tmppath, path)
393 | end
394 | if not status then
395 | if File.exists(tmppath) then
396 | File.unlink(tmppath)
397 | end
398 | module:fail_json({path=path, msg='Error while replacing ' .. errstr})
399 | end
400 | else
401 | local status, errstr, errno
402 | if state == 'hard' then
403 | status, errstr, errno = File.link(src, path)
404 | else
405 | status, errstr, errno = File.symlink(src, path)
406 | end
407 | if not status then
408 | module:fail_json({path=path, msg='Error while linking: ' .. errstr})
409 | end
410 | end
411 | end
412 |
413 | if module:check_mode() and not File.exists(path) then
414 | module:exit_json({dest=path, src=src, msg="dummy", changed=changed, diff=diff})
415 | end
416 |
417 | changed = File.set_fs_attributes_if_different(module, params, changed, diff)
418 | module:exit_json({dest=path, src=src, msg="dummy", changed=changed, diff=diff})
419 |
420 | elseif state == 'touch' then
421 | if not module:check_mode() then
422 | local status, errmsg
423 | if prev_state == 'absent' then
424 | status, errmsg = File.touch(path)
425 | if not status then
426 | module:fail_json({path=path, msg='Error, could not touch target: ' .. errmsg})
427 | end
428 | elseif prev_state == 'file' or prev_state == 'directory' or prev_state == 'hard' then
429 | status, errmsg = File.utime(path)
430 | if not status then
431 | module:fail_json({path=path, msg='Error while touching existing target: ' .. errmsg})
432 | end
433 | else
434 | module:fail_json({msg='Cannot touch other than files, directories, and hardlinks (' .. path .. " is " .. prev_state .. ")"})
435 | end
436 |
437 | -- FIXME: SORRY, we can't replicate the catching of SystemExit as far as I know...
438 | -- so we _may_ leak a file
439 | File.set_fs_attributes_if_different(module, params, true, diff)
440 | end
441 |
442 | module:exit_json({dest=path, changed=true, diff=diff, msg="dummy"})
443 | end
444 |
445 | module.fail_json({path=path, msg='unexpected position reached'})
446 | end
447 |
448 | main(arg)
449 |
--------------------------------------------------------------------------------
/src/fileutils.lua:
--------------------------------------------------------------------------------
1 | local FileUtil = {}
2 |
3 | local unistd = require("posix.unistd")
4 | local stat = require("posix.sys.stat")
5 | local stdlib = require("posix.stdlib")
6 | local libgen = require("posix.libgen")
7 | local pwd = require("posix.pwd")
8 | local grp = require("posix.grp")
9 | local os = require("os")
10 | local bm = require("BinDecHex")
11 | local perrno = require("posix.errno")
12 | local utime = require("posix.utime")
13 | local stdio = require("posix.stdio")
14 | local dirent = require("posix.dirent")
15 |
16 | FileUtil.__index = FileUtil
17 |
18 | function FileUtil.md5(module, path)
19 | local command = string.format("md5sum %q", path)
20 | local res, out, err = module:run_command(command)
21 |
22 | if res ~= 0 then
23 | module:fail_json({msg="Failed to determine the md5sum for " .. path, error=err})
24 | end
25 |
26 | local md5sum = string.match(out, "^[^%s\n]+")
27 |
28 | return md5sum
29 | end
30 |
31 | function FileUtil.sha1(module, path)
32 | local command = string.format("sha1sum %q", path)
33 | local res, out, err = module:run_command(command)
34 |
35 | if res ~= 0 then
36 | module:fail_json({msg="Failed to determine the sha1sum for " .. path, error=err})
37 | end
38 |
39 | local sha1sum = string.match(out, "^[^%s\n]+")
40 |
41 | return sha1sum
42 | end
43 |
44 | function FileUtil.expanduser(path)
45 | if path == nil then
46 | return nil
47 | end
48 | local home = os.getenv("HOME")
49 |
50 | return string.gsub(path, "^~", home)
51 | end
52 |
53 | function FileUtil.lexists(path)
54 | local status, errstr, errno = unistd.access(path, "f")
55 |
56 | return 0 == status, errstr, errno
57 | end
58 |
59 | function FileUtil.exists(path)
60 | local status, errstr, errno = unistd.access(path, "f")
61 |
62 | return 0 == status, errstr, errno
63 | end
64 |
65 | function FileUtil.readable(path)
66 | local status, errstr, errno = unistd.access(path, "r")
67 |
68 | return 0 == status, errstr, errno
69 | end
70 |
71 | function FileUtil.writeable(path)
72 | local status, errstr, errno = unistd.access(path, "w")
73 |
74 | return 0 == status, errstr, errno
75 | end
76 |
77 | function FileUtil.isdir(path)
78 | local pstat = stat.stat(path)
79 |
80 | if pstat then
81 | return 0 ~= stat.S_ISDIR(pstat['st_mode'])
82 | else
83 | return false
84 | end
85 | end
86 |
87 | function FileUtil.islnk(path)
88 | local pstat = stat.lstat(path)
89 |
90 | if pstat then
91 | return 0 ~= stat.S_ISLNK(pstat['st_mode'])
92 | else
93 | return false
94 | end
95 | end
96 |
97 | function FileUtil.stat(path)
98 | return stat.stat(path)
99 | end
100 |
101 | function FileUtil.lstat(path)
102 | return stat.lstat(path)
103 | end
104 |
105 | function FileUtil.realpath(path)
106 | return stdlib.realpath(path)
107 | end
108 |
109 | function FileUtil.readlink(path)
110 | return unistd.readlink(path)
111 | end
112 |
113 | function FileUtil.basename(path)
114 | return libgen.basename(path)
115 | end
116 |
117 | function FileUtil.dirname(path)
118 | return libgen.dirname(path)
119 | end
120 |
121 | function FileUtil.rmtree(path, opts)
122 | local args = "-r"
123 |
124 | if opts['ignore_errors'] then
125 | args = args .. "f"
126 | end
127 |
128 | local cmd = string.format("rm %s %q", args, path)
129 |
130 | local rc = nil
131 | if 5.1 < get_version() then
132 | _, _, rc = os.execute(cmd)
133 | else
134 | rc = os.execute(cmd)
135 | end
136 |
137 | return rc ~= 0
138 | end
139 |
140 | function FileUtil.unlink(path)
141 | local status, errstr, errno = unistd.unlink(path)
142 |
143 | return 0 == status, errstr, errno
144 | end
145 |
146 | function FileUtil.get_user_and_group(path)
147 | local stat = FileUtil.stat(path)
148 | if stat then
149 | return stat['st_uid'], stat['st_gid']
150 | else
151 | return nil, nil
152 | end
153 | end
154 |
155 | function FileUtil.parse_owner(owner)
156 | local uid = tonumber(owner)
157 | if (uid == nil) then
158 | local pwnam = pwd.getpwnam(owner)
159 | if pwnam ~= nil then
160 | uid = pwnam['pw_uid']
161 | end
162 | end
163 | return uid
164 | end
165 |
166 | function FileUtil.parse_group(group)
167 | local gid = tonumber(group)
168 | if (gid == nil) then
169 | local grnam = grp.getgrnam(group)
170 | if grnam ~= nil then
171 | gid = grnam['gr_gid']
172 | end
173 | end
174 | return gid
175 | end
176 |
177 | function FileUtil.lchown(path, uid, gid)
178 | local ret, errstr, errno
179 | -- lchown is only present in luaposix since 30.07.2016
180 | if unistd['lchown'] then
181 | ret, errstr, errno = unistd.lchown(path, uid, gid)
182 | else
183 | ret, errstr, errno = unistd.chown(path, uid, gid)
184 | end
185 | return ret == 0, errstr, errno
186 | end
187 |
188 | function FileUtil.set_owner_if_different(module, path, owner, changed, diff)
189 | path = FileUtil.expanduser(path)
190 | if owner == nil then
191 | return changed
192 | end
193 | local orig_uid, orig_gid = FileUtil.get_user_and_group(path)
194 | local uid = FileUtil.parse_owner(owner)
195 | if nil == uid then
196 | module:fail_json({path=path, msg='chown failed: failed to look up user ' .. tostring(owner)})
197 | end
198 | if orig_uid ~= uid then
199 | if nil ~= diff then
200 | if nil == diff['before'] then
201 | diff['before'] = {}
202 | end
203 | diff['before']['owner'] = orig_uid
204 | if nil == diff['after'] then
205 | diff['after'] = {}
206 | end
207 | diff['after']['owner'] = uid
208 | end
209 |
210 | if module:check_mode() then
211 | return true
212 | end
213 | -- FIXME: sorry if there is no chown we fail the sematic slightly... but i don't care
214 | if not FileUtil.lchown(path, uid, -1) then
215 | module:fail_json({path=path, msg='chown failed'})
216 | end
217 | changed = true
218 | end
219 | return changed
220 | end
221 |
222 | function FileUtil.set_group_if_different(module, path, group, changed, diff)
223 | path = FileUtil.expanduser(path)
224 | if group == nil then
225 | return changed
226 | end
227 | local orig_uid, orig_gid = FileUtil.get_user_and_group(path)
228 | local gid = FileUtil.parse_group(group)
229 | if nil == gid then
230 | module:fail_json({path=path, msg='chgrp failed: failed to look up group ' .. tostring(group)})
231 | end
232 | if orig_gid ~= gid then
233 | if nil ~= diff then
234 | if nil == diff['before'] then
235 | diff['before'] = {}
236 | end
237 | diff['before']['group'] = orig_gid
238 | if nil == diff['after'] then
239 | diff['after'] = {}
240 | end
241 | diff['after']['group'] = gid
242 | end
243 |
244 | if module:check_mode() then
245 | return true
246 | end
247 | -- FIXME: sorry if there is no chown we fail the sematic slightly... but i don't care
248 | if not FileUtil.lchown(path, -1, gid) then
249 | module:fail_json({path=path, msg='chgrp failed'})
250 | end
251 | changed = true
252 | end
253 | return changed
254 | end
255 |
256 | local function tohex(int)
257 | return bm.Dec2Hex(string.format("%d", int))
258 | end
259 |
260 | function FileUtil.S_IMODE(mode)
261 | -- man 2 stat
262 | -- "... and the least significant 9 bits (0777) as the file permission bits"
263 | return tonumber(bm.Hex2Dec(bm.BMAnd(tohex(mode), tohex(0x1ff))))
264 | end
265 |
266 | function FileUtil.lchmod(path, mode)
267 | if not FileUtil.islnk(path) then
268 | local ret, errstr, errno = stat.chmod(path, mode)
269 | return ret == 0, errstr, errno
270 | end
271 | return true, nil, nil
272 | end
273 |
274 | function FileUtil.set_mode_if_different(module, path, mode, changed, diff)
275 | path = FileUtil.expanduser(path)
276 | local path_stat = FileUtil.lstat(path)
277 |
278 | if mode == nil then
279 | return changed
280 | end
281 |
282 | if type(mode) ~= "number" then
283 | mode = tonumber(mode, 8)
284 | if nil == mode then
285 | module:fail_json({path=path, msg="mode must be in octal form (currently symbolic form is not supported, sorry)"})
286 | end
287 | end
288 | if mode ~= FileUtil.S_IMODE(mode) then
289 | -- prevent mode from having extra info or being invald long number
290 | module:fail_json({path=path, msg="Invalid mode supplied, only permission info is allowed", details=mode})
291 | end
292 |
293 | local prev_mode = FileUtil.S_IMODE(path_stat['st_mode'])
294 |
295 | if prev_mode ~= mode then
296 | if nil ~= diff then
297 | if nil == diff['before'] then
298 | diff['before'] = {}
299 | end
300 | diff['before']['mode'] = string.format("%o", prev_mode)
301 | if nil == diff['after'] then
302 | diff['after'] = {}
303 | end
304 | diff['after']['mode'] = string.format("%o", mode)
305 | end
306 |
307 | if module:check_mode() then
308 | return true
309 | end
310 |
311 | local res, errstr, errno = FileUtil.lchmod(path, mode)
312 | if not res then
313 | if errno ~= perrno['EPERM'] and errno ~= perrno['ELOOP'] then
314 | module:fail_json({path=path, msg='chmod failed', details=errstr})
315 | end
316 | end
317 |
318 | path_stat = FileUtil.lstat(path)
319 | local new_mode = FileUtil.S_IMODE(path_stat['st_mode'])
320 |
321 | if new_mode ~= prev_mode then
322 | changed = true
323 | end
324 | end
325 | return changed
326 | end
327 |
328 | function FileUtil.set_fs_attributes_if_different(module, file_args, changed, diff)
329 | changed = FileUtil.set_owner_if_different(module, file_args['path'], file_args['owner'], changed, diff)
330 | changed = FileUtil.set_group_if_different(module, file_args['path'], file_args['group'], changed, diff)
331 | changed = FileUtil.set_mode_if_different(module, file_args['path'], file_args['mode'], changed, diff)
332 | return changed
333 | end
334 |
335 | function FileUtil.isabs(path)
336 | return 1 == string.find(path, "/")
337 | end
338 |
339 | function FileUtil.mkdir(path)
340 | local status, errstr, errno = stat.mkdir(path)
341 | return 0 == status, errstr, errno
342 | end
343 |
344 | function FileUtil.walk(path, follow)
345 | local entries = {}
346 | local stack = {path}
347 | local i = 1
348 | while i <= #stack do
349 | local cur = stack[i]
350 |
351 | local ok, dir = pcall(dirent.dir, cur)
352 |
353 | local entry = { root=cur }
354 | local dirs = {}
355 | local files = {}
356 | if ok and dir ~= nil then
357 | for _, entry in ipairs(dir) do
358 | if "." ~= entry and ".." ~= entry then
359 | local child = cur .. "/" .. entry
360 | if follow and FileUtil.islnk(child) then
361 | local dst = FileUtil.realpath(child)
362 | dirs[#dirs + 1] = entry
363 | stack[#stack + 1] = dst
364 | elseif FileUtil.isdir(child) then
365 | dirs[#dirs + 1] = entry
366 | stack[#stack + 1] = child
367 | else
368 | files[#files + 1] = entry
369 | end
370 | end
371 | end
372 | end
373 | entry['dirs'] = dirs
374 | entry['files'] = files
375 | entries[#entries + 1] = entry
376 | i = i + 1
377 | end
378 |
379 | return entries
380 | end
381 |
382 | function FileUtil.listdir(path)
383 | local ok, dir = pcall(dirent.dir, path)
384 | if not ok then
385 | return nil
386 | end
387 |
388 | local entries = {}
389 |
390 | for _, k in ipairs(dir) do
391 | if k ~= "." and k ~= ".." then
392 | entries[#entries + 1] = k
393 | end
394 | end
395 |
396 | return entries
397 | end
398 |
399 | function FileUtil.rmdir(path)
400 | local status, errstr, errno = unistd.rmdir(path)
401 |
402 | return 0 == status, errstr, errno
403 | end
404 |
405 | function FileUtil.link(target, link)
406 | local status, errstr, errno = unistd.link(target, link, false)
407 |
408 | return 0 == status, errstr, errno
409 | end
410 |
411 | function FileUtil.symlink(target, link)
412 | local status, errstr, errno = unistd.link(target, link, true)
413 |
414 | return 0 == status, errstr, errno
415 | end
416 |
417 | function FileUtil.unlink(path)
418 | local status, errstr, errno = unistd.unlink(path)
419 |
420 | return 0 == status, errstr, errno
421 | end
422 |
423 | function FileUtil.touch(path)
424 | local file, errmsg = io.open(path, "w")
425 | if file ~= nil then
426 | io.close(file)
427 | end
428 | return file ~= nil, errmsg
429 | end
430 |
431 | function FileUtil.utime(path)
432 | local status, errstr, errno = utime.utime(path)
433 |
434 | return 0 == status, errstr, errno
435 | end
436 |
437 | function FileUtil.join(path, paths)
438 | for _, segment in ipairs(paths) do
439 | if segment ~= nil then
440 | if FileUtil.isabs(segment) then
441 | path = segment
442 | else
443 | path = path .. "/" .. segment
444 | end
445 | end
446 | end
447 |
448 | return path
449 | end
450 |
451 | function FileUtil.rename(oldpath, newpath)
452 | local status, errstr, errno
453 | if nil ~= stdio['rename'] then
454 | status, errstr, errno = stdio.rename(oldpath, newpath)
455 | status = status == 0
456 | else
457 | status, errstr, errno = os.rename(oldpath, newpath)
458 | end
459 |
460 | return status, errstr, errno
461 | end
462 |
463 | function FileUtil.split(path)
464 | local tail = FileUtil.basename(path)
465 | local head = FileUtil.dirname(path)
466 | return head, tail
467 | end
468 |
469 | function FileUtil.split_pre_existing_dir(dirname)
470 | -- Return the first pre-existing directory and a list of the new directories that will be created
471 | local head, tail = FileUtil.split(dirname)
472 |
473 | local pre_existing_dir, new_directory_list
474 | if not FileUtil.exists(head) then
475 | pre_existing_dir, new_directory_list = FileUtil.split_pre_existing_dir(head)
476 | else
477 | return head, {tail}
478 | end
479 | new_directory_list[#new_directory_list + 1] = tail
480 | return pre_existing_dir, new_directory_list
481 | end
482 |
483 | function FileUtil.mkdirs(path)
484 | local exists, new = FileUtil.split_pre_existing_dir(path)
485 |
486 | for _, seg in ipairs(new) do
487 | exists = exists .. "/" .. seg
488 | local res, errstr, errno = FileUtil.mkdir(exists)
489 | if not res then
490 | return res, errstr, errno
491 | end
492 | end
493 | return true
494 | end
495 |
496 | function FileUtil.mkstemp(pattern)
497 | local fd, path = stdlib.mkstemp(pattern)
498 | if -1 ~= fd and type(fd) == "number" then
499 | unistd.close(fd)
500 | return path
501 | else
502 | return nil, path -- path is a errmsg in this case
503 | end
504 | end
505 |
506 | return FileUtil
507 |
--------------------------------------------------------------------------------
/src/lineinfile.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local File = require("fileutils")
5 |
6 | local function join(list, sep)
7 | local cur = ""
8 | for i, v in ipairs(list) do
9 | if i ~= 1 then
10 | cur = string.format("%s%s%s", cur, sep, v)
11 | else
12 | cur = v
13 | end
14 | end
15 | return cur
16 | end
17 |
18 | function write_changes(module, lines, dest)
19 | -- FIXME: we do not support validate, sorry
20 | module:unslurp(dest, join(lines, "\n") .. "\n")
21 | end
22 |
23 | function check_file_attrs(module, changed, message, diff)
24 | file_args = module:get_params()
25 | if File.set_fs_attributes_if_different(module, file_args, changed, diff) then
26 | if changed then
27 | message = message .. " and "
28 | end
29 | changed = true
30 | message = message .. "ownership or perms changed"
31 | end
32 |
33 | return message, changed
34 | end
35 |
36 | local function splitlines(content)
37 | local lines = {}
38 | for line in string.gmatch(content, "[^\n]+") do
39 | lines[#lines + 1] = line
40 | end
41 | return lines
42 | end
43 |
44 | local function append(t1, t2)
45 | for k,v in ipairs(t2) do
46 | t1[#t1 + 1] = v
47 | end
48 | return t1
49 | end
50 |
51 |
52 | local function rstrip(str, chars)
53 | return string.gsub(str, string.format("[%s]+$", chars), "")
54 | end
55 |
56 | local function filter(matcher, list)
57 | local tmp = {}
58 | for i,v in ipairs(list) do
59 | if matcher(v) then
60 | tmp[#tmp + 1] = v
61 | end
62 | end
63 | return tmp
64 | end
65 |
66 | function present(module, dest, regexp, line, insertafter, insertbefore, create, backup, backrefs)
67 | diff = {before="", after="", before_header=dest .. " (content)", after_header=dest .. " (content)"}
68 |
69 | local lines
70 | if not File.exists(dest) then
71 | if not create then
72 | module:fail_json({rc=257, msg='Destination ' .. dest .. ' does not exist!'})
73 | end
74 | local destpath = File.dirname(dest)
75 | if not File.exists(destpath) and not module:check_mode() then
76 | local status, errstr = File.mkdirs(destpath)
77 | if not status then
78 | module:fail_json({msg="Failed to create path components for " .. destpath .. ": " .. errstr})
79 | end
80 | end
81 | lines = {}
82 | else
83 | lines = splitlines(module:slurp(dest))
84 | end
85 |
86 | if module._diff then
87 | diff['before'] = join(lines, "\n")
88 | end
89 |
90 | local mre = regexp
91 |
92 | local insre = nil
93 | if insertafter ~= nil and insertafter ~= 'BOF' and insertafter ~= 'EOF' then
94 | insre = insertafter
95 | elseif insertbefore ~= nil and insertbefore ~= 'BOF' then
96 | insre = insertbefore
97 | end
98 |
99 |
100 | -- matchno is the line num where the regexp has been found
101 | -- borano is the line num where the insertafter/insertbefore has been found
102 | local matchno, borano = -1, -1
103 | local m = nil
104 | for lineno, cur_line in ipairs(lines) do
105 | if regexp ~= nil then
106 | -- FIXME: lua patterns are not regexes
107 | match_found = string.match(cur_line, mre)
108 | else
109 | match_found = line == rstrip(cur_line, '\r\n')
110 | end
111 | if match_found then
112 | matchno = lineno
113 | m = cur_line
114 | elseif insre ~= nil and string.match(cur_line, insre) then
115 | if insertafter then
116 | -- + 1 for the next line
117 | borano = lineno + 1
118 | end
119 | if insertbefore then
120 | -- + 1 for the previous line
121 | borano = lineno
122 | end
123 | end
124 | end
125 |
126 | local msg = ''
127 | local changed = false
128 |
129 | -- Regexp matched a line in the file
130 | if matchno ~= -1 then
131 | local new_line
132 | if backrefs then
133 | new_line = string.gsub(m, mre, line)
134 | else
135 | -- don't do backref expansion if not asked
136 | new_line = line
137 | end
138 |
139 | new_line = rstrip(new_line, '\r\n')
140 |
141 | if lines[matchno] ~= new_line then
142 | lines[matchno] = new_line
143 | msg = 'line replaced'
144 | changed = true
145 | end
146 | elseif backrefs then
147 | -- Do absolutely nothing since it's not safe generating the line
148 | -- without the regexp matching to populate the backrefs
149 | elseif insertbefore == 'BOF' or insertafter=='BOF' then
150 | local tmp = { line }
151 | lines = append(tmp, lines)
152 | msg = 'line added'
153 | changed = true
154 | -- Add it to the end of the file if requested or
155 | -- if insertafter/insertbefore didn't match anything
156 | -- (so default behaviour is to add at the end)
157 | elseif insertafter == 'EOF' or borano == -1 then
158 | lines[#lines + 1] = line
159 | msg = 'line added'
160 | changed = true
161 | -- insert* matched, but not the regexp
162 | else
163 | local tmp = {}
164 | for i,v in ipairs(lines) do
165 | if i == borano then
166 | tmp[#tmp + 1] = line
167 | end
168 | tmp[#tmp + 1] = v
169 | end
170 | end
171 |
172 | if module._diff then
173 | diff['after'] = join(lines, "\n")
174 | end
175 |
176 | local backupdest = ""
177 | if changed and not module:check_mode() then
178 | if backup and File.exists(dest) then
179 | backupdest = module:backup_local(dest)
180 | end
181 | write_changes(module, lines, dest)
182 | end
183 |
184 | if module:check_mode() and not File.exists(dest) then
185 | module:exit_json({changed=changed, msg=msg, backup=backupdest, diff=diff})
186 | end
187 |
188 | local attr_diff = {}
189 | msg, changed = check_file_attrs(module, changed, msg, attr_diff)
190 |
191 | attr_diff['before_header'] = dest .. " (file attributes)"
192 | attr_diff['after_header'] = dest .. " (file attributes)"
193 |
194 | local difflist = {diff, attr_diff}
195 | module:exit_json({changed=changed, msg=msg, backup=backupdest, diff=difflist})
196 | end
197 |
198 | function absent(module, dest, regexp, line, backup)
199 | if not File.exists(dest) then
200 | module:exit_json({changed=false, msg="file not present"})
201 | end
202 |
203 | local msg = ""
204 | diff = {before='', after='', before_header=dest .. '(content)', after_header=dest .. '(content)'}
205 |
206 | local lines = splitlines(module:slurp(dest))
207 |
208 | if module._diff then
209 | diff['before'] = join(lines, "\n")
210 | end
211 |
212 | local cre
213 | if regexp ~= nil then
214 | cre = regexp
215 | end
216 | found = {}
217 |
218 | local function matcher(cur_line)
219 | local match_found
220 | if regexp ~= nil then
221 | match_found = string.match(cur_line, cre)
222 | else
223 | match_found = line == rstrip(cur_line, "\r\n")
224 | end
225 | if match_found then
226 | found[#found + 1] = cur_line
227 | end
228 |
229 | return not match_found
230 | end
231 |
232 | lines = filter(matcher, lines)
233 | changed = #found > 0
234 |
235 | if module._diff then
236 | diff['after'] = join(lines, "\n")
237 | end
238 |
239 | backupdest = ""
240 | if changed and not module:check_mode() then
241 | if backup then
242 | backupdest = module:backup_local(dest)
243 | end
244 | write_changes(module, lines, dest)
245 | end
246 |
247 | if changed then
248 | msg = tostring(#found) .. " line(s) removed"
249 | end
250 |
251 | local attr_diff={}
252 | attr_diff['before_header'] = dest .. " (file attributes)"
253 | attr_diff['after_header'] = dest .. " (file attributes)"
254 |
255 | local difflist = {diff, attr_diff}
256 | module:exit_json({changed=changed, found=#found, msg=msg, backup=backupdest, diff=difflist})
257 | end
258 |
259 | function main(arg)
260 | local module = Ansible.new({
261 | line = { type='str' },
262 | mode = { type='str' },
263 | backup = { default=false, type='bool' },
264 | insertbefore = { type='str' },
265 | insertafter = { type='str' },
266 | owner = { type='str' },
267 | group = { type='str' },
268 | backrefs = { default=false, type='bool' },
269 | create = { default=false, type='bool' },
270 | path = { aliases={'name', 'dest', 'destfile'}, type='path', required='true' },
271 | regexp = { type='str' },
272 | state = { default = "present", choices={"present", "absent"} },
273 | })
274 |
275 | module:parse(arg[1])
276 |
277 | local p = module:get_params()
278 |
279 | -- Ensure that the dest parameter is valid
280 | local dest = File.expanduser(p['path'])
281 | local create = p['create']
282 | local backup = p['backup']
283 | local backrefs = p['backrefs']
284 |
285 | if p['insertbefore'] and p['insertafter'] then
286 | module:fail_json({msg="The options insertbefore and insertafter are mutually exclusive"})
287 | end
288 |
289 | if File.isdir(dest) then
290 | module:fail_json({msg="Destination " .. dest .. " is a directory!"})
291 | end
292 |
293 | if p['state'] == "present" then
294 | if backrefs and p['regexp'] == nil then
295 | module:fail_json({msg='regexp= is required wioth backrefs=true'})
296 | end
297 |
298 | if p['line'] == nil then
299 | module:fail_json({msg='line= is required with state=present'})
300 | end
301 |
302 | -- Deal with the insertafter default value manually, to avoid errors
303 | -- because of the mutually_exclusive mechanism
304 | local ins_bef, ins_aft = p['insertbefore'], p['insertafter']
305 | if ins_bef == nil and ins_aft == nil then
306 | ins_aft = 'EOF'
307 | end
308 |
309 | local line = p['line']
310 | present(module, dest, p['regexp'], line, ins_aft, ins_bef, create, backup, backrefs)
311 | else
312 | if p['regexp'] == nil and p['line'] == nil then
313 | module:fail_json({msg='one of line= or regexp= is required with state=absent'})
314 | end
315 | absent(module, dest, p['regexp'], p['line'], backup)
316 | end
317 | end
318 |
319 | main(arg)
320 |
--------------------------------------------------------------------------------
/src/opkg.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 |
5 | function update_package_db(module, opkg_path)
6 | local rc, out, err = module:run_command(string.format("%s update", opkg_path))
7 |
8 | if rc ~= 0 then
9 | module:fail_json({msg = "could not update package db", opkg={rc=rc, out=out, err=err}})
10 | end
11 | end
12 |
13 | function query_package(module, opkg_path, name)
14 | local rc, out, err = module:run_command(string.format("%s list-installed", opkg_path))
15 |
16 | if rc ~= 0 then
17 | module:fail_json({msg = "failed to list installed packages", opkg={rc=rc, out=out, err=err}})
18 | end
19 |
20 | for line in string.gmatch(out, "[^\n]+") do
21 | if name == string.match(line, "^(%S+)%s") then
22 | return true
23 | end
24 | end
25 |
26 | return false
27 | end
28 |
29 | function get_force(force)
30 | if force and string.len(force) > 0 then
31 | return "--force-" .. force
32 | else
33 | return ""
34 | end
35 | end
36 |
37 | function remove_packages(module, opkg_path, packages)
38 | local p = module:get_params()
39 |
40 | local force = get_force(p["force"])
41 |
42 | local remove_c = 0
43 |
44 | for _,package in ipairs(packages) do
45 | -- Query the package first, to see if we even need to remove
46 | if query_package(module, opkg_path, package) then
47 | if not module:check_mode() then
48 | local rc, out, err = module:run_command(string.format("%s remove %s %q", opkg_path, force, package))
49 |
50 | if rc ~= 0 or query_package(module, opkg_path, package) then
51 | module:fail_json({msg="failed to remove " .. package, opkg={rc=rc, out=out, err=err}})
52 | end
53 | end
54 |
55 | remove_c = remove_c + 1;
56 | end
57 | end
58 |
59 | if remove_c > 0 then
60 | module:exit_json({changed=true, msg=string.format("removed %d package(s)", remove_c)})
61 | else
62 | module:exit_json({changed=false, msg="package(s) already absent"})
63 | end
64 | end
65 |
66 | function install_packages(module, opkg_path, packages)
67 | local p = module:get_params()
68 |
69 | local force = get_force(p["force"])
70 |
71 | local install_c = 0
72 |
73 | for _,package in ipairs(packages) do
74 | -- Query the package first, to see if we even need to remove
75 | if not query_package(module, opkg_path, package) then
76 | if not module:check_mode() then
77 | local rc, out, err = module:run_command(string.format("%s install %s %s", opkg_path, force, package))
78 |
79 | if rc ~= 0 or not query_package(module, opkg_path, package) then
80 | module:fail_json({msg=string.format("failed to install %s", package), opkg={rc=rc, out=out, err=err}})
81 | end
82 | end
83 |
84 | install_c = install_c + 1;
85 | end
86 | end
87 |
88 | if install_c > 0 then
89 | module:exit_json({changed=true, msg=string.format("installed %s packages(s)", install_c)})
90 | else
91 | module:exit_json({changed=false, msg="package(s) already present"})
92 | end
93 | end
94 |
95 | function last_cache_update_timestamp(module)
96 | local rc, stdout, stderr = module:run_command("date +%s -r /tmp/opkg-lists")
97 | if rc ~= 0 then
98 | return nil
99 | else
100 | return tonumber(stdout)
101 | end
102 | end
103 |
104 | function current_timestamp(module)
105 | local rc, stdout, stderr = module:run_command("date +%s")
106 | return tonumber(stdout)
107 | end
108 |
109 | function cache_age(module)
110 | local last_update = last_cache_update_timestamp(module)
111 | if last_update == nil then
112 | return nil
113 | else
114 | return current_timestamp(module) - last_update
115 | end
116 | end
117 |
118 | function should_update_cache(module)
119 | if module.params.update_cache then
120 | return true
121 | end
122 | if module.params.cache_valid_time ~= nil then
123 | local age = cache_age(module)
124 | if age == nil then
125 | return true
126 | end
127 | if age > module.params.cache_valid_time then
128 | return true
129 | end
130 | end
131 | return false
132 | end
133 |
134 | function update_cache_if_needed(module, opkg_path)
135 | if should_update_cache(module) and not module:check_mode() then
136 | update_package_db(module, opkg_path)
137 | end
138 | end
139 |
140 | function main(arg)
141 | local module = Ansible.new({
142 | name = { aliases = {"pkg"}, required=true , type='list'},
143 | state = { default = "present", choices={"present", "installed", "absent", "removed"} },
144 | force = { default = "", choices={"", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"} } ,
145 | update_cache = { default = "no", aliases={ "update-cache" }, type='bool' },
146 | cache_valid_time = { type='int' }
147 | })
148 |
149 | local opkg_path = module:get_bin_path('opkg', true, {'/bin'})
150 |
151 | module:parse(arg[1])
152 |
153 | local p = module:get_params()
154 |
155 | update_cache_if_needed(module, opkg_path)
156 |
157 | local state = p["state"]
158 | local packages = p["name"]
159 | if "present" == state or "installed" == state then
160 | install_packages(module, opkg_path, packages)
161 | elseif "absent" == state or "removed" == state then
162 | remove_packages(module, opkg_path, packages)
163 | end
164 | end
165 |
166 | main(arg)
167 |
--------------------------------------------------------------------------------
/src/ping.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 |
5 | function main(arg)
6 | local module = Ansible.new({
7 | data = { default="pong" },
8 | })
9 |
10 | module:parse(arg[1])
11 |
12 | local p = module:get_params()
13 |
14 | local data = p["data"]
15 |
16 | if "crash" == data then
17 | module:fail_json({ msg="boom" })
18 | else
19 | module:exit_json({ changed=false, ping=data })
20 | end
21 | end
22 |
23 | main(arg)
24 |
--------------------------------------------------------------------------------
/src/slurp.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local base64 = require("base64")
5 |
6 | function main(arg)
7 | local module = Ansible.new({
8 | src = { required=true, type="path", aliases={"path"} },
9 | })
10 |
11 | module:parse(arg[1])
12 |
13 | local source = module:get_params()["src"]
14 | local content = module:slurp(source)
15 | local encoded = base64.encode(content)
16 |
17 | module:exit_json({content=encoded, source=source, encoding='base64'})
18 | end
19 |
20 | main(arg)
21 |
--------------------------------------------------------------------------------
/src/stat.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local File = require("fileutils")
5 | local stat = require("posix.sys.stat")
6 | local errno = require("posix.errno")
7 | local bm = require("BinDecHex")
8 | local stdlib = require("posix.stdlib")
9 | local unistd = require("posix.unistd")
10 | local pwd = require("posix.pwd")
11 | local grp = require("posix.grp")
12 |
13 | local function tohex(int)
14 | return bm.Dec2Hex(string.format("%d", int))
15 | end
16 |
17 | local function S_IMODE(mode)
18 | -- man 2 stat
19 | -- "... and the least significant 9 bits (0777) as the file permission bits"
20 | return tonumber(bm.Hex2Dec(bm.BMAnd(tohex(mode), tohex(0x1ff))))
21 | end
22 |
23 | local function boolmask(mode, mask)
24 | local masked = tonumber(bm.Hex2Dec(bm.BMAnd(tohex(mode), tohex(mask))))
25 |
26 | if 0 == masked then
27 | return false
28 | else
29 | return true
30 | end
31 | end
32 |
33 | function main(arg)
34 | local module = Ansible.new(
35 | { path = { required=true, type='path' }
36 | , follow = { default=false, type='bool' }
37 | , get_md5 = { default=true, type='bool'}
38 | , get_checksum = { default=true, type='bool' }
39 | , checksum_algorithm = { default='sha1', type='str', choices={'sha1'}, aliases={'checksum_algo', 'checksum'}}
40 | }
41 | )
42 |
43 | module:parse(arg[1])
44 |
45 | local p = module:get_params()
46 |
47 | local path = p['path']
48 | local follow = p['follow']
49 | local get_md5 = p['get_md5']
50 | local get_checksum = p['get_checksum']
51 | local checksum_algorithm = p['checksum_algorithm']
52 |
53 | local st, err, rc
54 | if follow then
55 | st, err, rc = stat.stat(path)
56 | else
57 | st, err, rc = stat.lstat(path)
58 | end
59 |
60 | if not st then
61 | if rc == errno.ENOENT then
62 | d = { exists=false }
63 | module:exit_json({msg="No such file exists", changed=false, stat=d})
64 | end
65 |
66 | module:fail_json({msg=err})
67 | end
68 |
69 | mode = st['st_mode']
70 |
71 | -- back to ansible
72 | d = {
73 | exists = true
74 | , path = path
75 | , mode = string.format("%04o", S_IMODE(mode))
76 | , isdir = stat.S_ISDIR(mode)
77 | , ischr = stat.S_ISCHR(mode)
78 | , isblk = stat.S_ISBLK(mode)
79 | , isreg = stat.S_ISREG(mode)
80 | , isfifo = stat.S_ISFIFO(mode)
81 | , islnk = stat.S_ISLNK(mode)
82 | , issock = stat.S_ISSOCK(mode)
83 | , uid = st['st_uid']
84 | , gid = st['st_gid']
85 | , size = st['st_size']
86 | , inode = st['st_ino']
87 | , dev = st['st_dev']
88 | , nlink = st['st_nlink']
89 | , atime = st['st_atime']
90 | , mtime = st['st_mtime']
91 | , ctime = st['ctime']
92 | , wusr = boolmask(mode, stat.S_IWUSR)
93 | , rusr = boolmask(mode, stat.S_IRUSR)
94 | , xusr = boolmask(mode, stat.S_IXUSR)
95 | , wgrp = boolmask(mode, stat.S_IWGRP)
96 | , rgrp = boolmask(mode, stat.S_IRGRP)
97 | , xgrp = boolmask(mode, stat.S_IXGRP)
98 | , woth = boolmask(mode, stat.S_IWOTH)
99 | , roth = boolmask(mode, stat.S_IROTH)
100 | , xoth = boolmask(mode, stat.S_IXOTH)
101 | , isuid = boolmask(mode, stat.S_ISUID)
102 | , isgid = boolmask(mode, stat.S_ISGID)
103 | }
104 |
105 | if 0 ~= d['islnk'] then
106 | d['lnk_source'] = stdlib.realpath(path)
107 | end
108 |
109 | if 0 ~= d['isreg'] and get_md5 and 0 == unistd.access(path, "r") then
110 | d['md5'] = File.md5(module, path)
111 | end
112 |
113 | if 0 ~= d['isreg'] and get_checksum and 0 == unistd.access(path, "r") then
114 | local chksums = { sha1=File.sha1 }
115 | d['checksum'] = chksums[p['checksum_algorithm']](module, path)
116 | end
117 |
118 | local pw = pwd.getpwuid(st['st_uid'])
119 | d['pw_name'] = pw['pw_name']
120 |
121 | local grp_info = grp.getgrgid(st['st_gid'])
122 | d['gr_name'] = grp_info['gr_name']
123 |
124 | d['mime_type'] = 'unknown'
125 | d['charset'] = 'unknown'
126 |
127 | module:exit_json({msg="Stat successful", changed=false, stat=d})
128 | end
129 |
130 | main(arg)
131 |
--------------------------------------------------------------------------------
/src/ubus.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local ubus = require("ubus")
5 | local json = require("dkjson")
6 |
7 | function list(module)
8 | check_parameters(module, {"path"})
9 | local path = module:get_params()['path']
10 |
11 | local conn = module:ubus_connect()
12 |
13 | local list = {}
14 |
15 | local namespaces = conn:objects()
16 | if not namespaces then
17 | module:fail_json({msg="Failed to enumerate ubus"})
18 | end
19 |
20 | for _, n in ipairs(namespaces) do
21 | if not path or Ansible.contains(n, path) then
22 | local signatures = conn:signatures(n)
23 | if not signatures then
24 | module:fail_json({msg="Failed to enumerate ubus"})
25 | end
26 | list[n] = signatures
27 | end
28 | end
29 |
30 | conn:close()
31 | module:exit_json({msg="Gathered local signatures", signatures=list})
32 | end
33 |
34 | function call(module)
35 | check_parameters(module, {"path", "method", "message"})
36 | local p = module:get_params()
37 | local path = p["path"]
38 | if 1 ~= #path then
39 | module:fail_json({msg="Call only allows one path element, but zero or 2+ were given"})
40 | else
41 | path = path[1]
42 | end
43 |
44 | local conn = module:ubus_connect()
45 | local res = module:ubus_call(conn, path, p['method'], p['message'])
46 |
47 | conn:close()
48 | module:exit_json({msg=string.format("Called %s.%s(%s)", path, p['method'], json.encode(p['message'])), result=res, changed=true})
49 | end
50 |
51 | function send(module)
52 | -- - send [] Send an event
53 | check_parameters(module, {"type", "message"})
54 | local p = module:get_params()
55 |
56 | local conn = module:ubus_connect()
57 |
58 | local res, status = conn:send(p["type"], p["message"])
59 | if not res then
60 | module:fail_json({msg="Failed to send event", status=status})
61 | end
62 |
63 | conn:close()
64 | module:exit_json({msg="Event sent successfully", result=res, changed=true})
65 | end
66 |
67 | function facts(module)
68 | check_parameters(module, {})
69 |
70 | local conn = module:ubus_connect()
71 |
72 | local facts = {}
73 |
74 | local namespaces = conn:objects()
75 | for _,n in ipairs(namespaces) do
76 | if "network.device" == n
77 | or 1 == string.find(n, "network.interface.")
78 | or "network.wireless" == n then
79 | facts[n] = module:ubus_call(conn, n, "status", {})
80 | elseif "service" == n then
81 | -- list {}
82 | facts[n] = module:ubus_call(conn, n, "list", {})
83 | elseif "system" == n then
84 | -- board {}
85 | -- info {}
86 | local f = {}
87 | f["board"] = module:ubus_call(conn, n, "board", {})
88 | f["info"] = module:ubus_call(conn, n, "info", {})
89 | facts[n] = f
90 | elseif "uci" == n then
91 | -- configs {}
92 | -- foreach configs...
93 | local f = {}
94 | local configs = module:ubus_call(conn, n, "configs", {})['configs']
95 | f["configs"] = configs
96 | f["state"] = {}
97 |
98 | for _,conf in ipairs(configs) do
99 | -- TODO: transform unnamed sections to their anonymous names
100 | f["state"][conf] = module:ubus_call( conn, n, "state", {config=conf})['values']
101 | end
102 | facts[n] = f
103 | end
104 | end
105 |
106 | conn:close()
107 |
108 | module:exit_json({msg="All available facts gathered", ansible_facts=facts})
109 | end
110 |
111 | function check_parameters(module, valid)
112 | local p = module:get_params()
113 | local i = 0
114 | for k,_ in pairs(p) do
115 | -- not a buildin command and not a valid entry
116 | if 1 ~= string.find(k, "_ansible")
117 | and k ~= "socket"
118 | and k ~= "timeout"
119 | and k ~= "command" then
120 |
121 | i = i+1
122 |
123 | if((not Ansible.contains(k, valid))) then
124 | module:fail_json({msg=string.format("Parameter %q invalid for command %s", k, p['command'])})
125 | end
126 | end
127 | end
128 |
129 | return i
130 | end
131 |
132 | function main(arg)
133 | -- module models the ubus cli tools structure
134 | -- Usage: ubus [] [arguments...]
135 | -- Options:
136 | -- -s : Set the unix domain socket to connect to
137 | -- -t : Set the timeout (in seconds) for a command to complete
138 | -- -S: Use simplified output (for scripts)
139 | -- -v: More verbose output
140 | --
141 | -- Commands:
142 | -- - list [] List objects
143 | -- - call [] Call an object method
144 | -- - send [] Send an event
145 |
146 | local module = Ansible.new({
147 | command = { aliases = {"cmd"}, required=true , choices={"list", "call", "send", "facts"}},
148 | path = { type="list" },
149 | method = { type="str" },
150 | type = { type="str" },
151 | message = { type="jsonarg" },
152 | socket = { type="path" },
153 | timeout = { type="int"}
154 | })
155 |
156 | module:parse(arg[1])
157 |
158 | local p = module:get_params()
159 |
160 | local dispatcher = {
161 | list = list,
162 | call = call,
163 | send = send,
164 | facts = facts
165 | }
166 |
167 | dispatcher[p['command']](module)
168 | end
169 |
170 | main(arg)
171 |
--------------------------------------------------------------------------------
/src/uci.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 |
3 | local Ansible = require("ansible")
4 | local ubus = require("ubus")
5 |
6 | function reload_configs(module)
7 | local conn = module:ubus_connect()
8 |
9 | local res = module:ubus_call(conn, "uci", "reload_config", {})
10 |
11 | conn:close()
12 | module:exit_json({msg="Configs reloaded", result=res})
13 | end
14 |
15 | function get_configs(module)
16 | local conn = module:ubus_connect()
17 |
18 | local res = module:ubus_call(conn, "uci", "configs", {})
19 |
20 | conn:close()
21 | module:exit_json({msg="Configs fetched", result=res})
22 | end
23 |
24 | function docommit(module, conn, config)
25 | local conf, sec = check_config(module, conn, config, nil)
26 |
27 | local res = module:ubus_call(conn, "uci", "commit", {config=conf})
28 |
29 | return res
30 | end
31 |
32 | function commit(module)
33 | local conn = module:ubus_connect()
34 | local path = module:get_params()["name"]
35 |
36 | local configs
37 | if path == nil then
38 | local conf = module:ubus_call(conn, "uci", "configs", {})
39 | configs = conf['configs']
40 | else
41 | if path["option"] or path["section"] then
42 | module:fail_json({msg="Only whole configs can be committed"})
43 | end
44 |
45 | configs = { path["config"] }
46 | end
47 |
48 | local res = {}
49 | for _, conf in ipairs(configs) do
50 | res[#res + 1] = docommit(module, conn, conf)
51 | end
52 |
53 | module:exit_json({msg="Committed all changes for " .. #configs .. " configurations", changed=true, result=res})
54 | end
55 |
56 | function get(module)
57 | local conn = module:ubus_connect()
58 | local p = module:get_params()
59 | local path = p["name"]
60 |
61 | local msg = {config=path["config"]}
62 | if p["match"] ~= nil then
63 | msg["match"] = p["match"]
64 | end
65 | if p["type"] ~= nil then
66 | msg["type"] = p["type"]
67 | end
68 | if path["section"] ~= nil then
69 | msg["section"] = path["section"]
70 | end
71 |
72 | local res = module:ubus_call(conn, "uci", "get", msg)
73 |
74 | module:exit_json({msg="Got config", changed=false, result=res})
75 | end
76 |
77 | function revert(module)
78 | local conn = module:ubus_connect()
79 | local path = module:get_params()["name"]
80 |
81 | local configs
82 | if path == nil then
83 | local conf = module:ubus_call(conn, "uci", "configs", {})
84 | configs = conf['configs']
85 | else
86 | local conf, sec = check_config(module, conn, path["config"], nil)
87 | configs = { conf }
88 | end
89 |
90 | local res = {}
91 | for _, conf in ipairs(configs) do
92 | res[#res + 1] = module:ubus_call(conn, "uci", "revert", {config=conf})
93 | end
94 |
95 | module:exit_json({msg="Successfully reverted all staged changes for " .. #configs .. " configurations", changed=true, result=res})
96 | end
97 |
98 | function parse_path(module)
99 | local path = module:get_params()['name']
100 | -- a path consists of config.section.option
101 |
102 | -- lua's pattern engine does not seem to be expressive enough to do this in one go
103 | local config, section, option
104 | if string.match(path, "([^.]+)%.([^.]+)%.([^.]+)") then
105 | config, section, option = string.match(path, "([^.]+)%.([^.]+)%.([^.]+)")
106 | elseif string.match(path, "([^.]+)%.([^.]+)") then
107 | config, section = string.match(path, "([^.]+)%.([^.]+)")
108 | else
109 | config = path
110 | end
111 |
112 | local pathobject = {config=config, section=section, option=option}
113 | return pathobject
114 | end
115 |
116 | function query_value(module, conn, path, unique)
117 | local res = conn:call("uci", "get", path)
118 |
119 | if nil == res then
120 | return nil
121 | end
122 |
123 | if unique and nil ~= res["values"] then
124 | module:fail_json({msg="Path specified is amiguos and matches multiple options", path=path, result=res})
125 | end
126 |
127 | if res["values"] then
128 | return res["values"]
129 | else
130 | return res["value"]
131 | end
132 | end
133 |
134 | function check_config(module, conn, config, section)
135 | local res = module:ubus_call(conn, "uci", "configs", {})
136 |
137 | if not module.contains(config, res["configs"]) then
138 | module:fail_json({msg="Invalid config " .. config})
139 | end
140 |
141 | if nil ~= section then
142 | res = module:ubus_call(conn, "uci", "get", {config=config, section=section})
143 | if res and res["values"] and res["values"][".type"] then
144 | return config, section
145 | end
146 | end
147 |
148 | return config, nil
149 | end
150 |
151 | function compare_tables(a, b)
152 | if a == nil or b == nil then
153 | return a == b
154 | end
155 |
156 | if type(a) ~= "table" then
157 | if type(b) ~= "table" then
158 | return a == b
159 | end
160 | return false
161 | end
162 | if #a ~= #b then
163 | return false
164 | end
165 | -- level 1 compare
166 | table.sort(a)
167 | table.sort(b)
168 | for i,v in ipairs(a) do
169 | if v ~= b[i] then
170 | return false
171 | end
172 | end
173 |
174 | return true
175 | end
176 |
177 | function set_value(module)
178 | local p = module:get_params()
179 | local path = p["name"]
180 |
181 | local conn = module:ubus_connect()
182 |
183 | local conf, sec = check_config(module, conn, path["config"], path["section"])
184 |
185 | local target = p["value"]
186 | local forcelist = p["forcelist"]
187 |
188 | if type(target) == "table" and #target == 1 and not forcelist then
189 | target = target[1]
190 | end
191 |
192 | local values = {}
193 | if path["option"] then
194 | values[path["option"]] = target
195 | end
196 |
197 | local res
198 | if nil ~= p["match"] then
199 | local preres = module:ubus_call(conn, "uci", "changes", {config=conf})
200 | local prechanges = preres["changes"] or {}
201 |
202 | local message = {
203 | config=conf,
204 | values=p["values"],
205 | match=p["match"]
206 | }
207 | res = module:ubus_call(conn, "uci", "set", message) or {}
208 |
209 | -- Since 'uci changes' returns changes in the order they were made,
210 | -- determine what the 'set' command changed by stripping off the
211 | -- first #prechanges entries from the postchanges.
212 | local postres = module:ubus_call(conn, "uci", "changes", {config=conf})
213 | local postchanges = postres["changes"] or {}
214 | for i = #prechanges, 1, -1 do
215 | table.remove(postchanges, i)
216 | end
217 | res["changes"] = postchanges
218 |
219 | conn:close()
220 | if #postchanges > 0 then
221 | module:exit_json({msg="Changes made", changed=true, result=res})
222 | end
223 | module:exit_json({msg="No changes made", changed=false, result=res})
224 | elseif not sec then
225 | -- We have to create a section and use "uci add"
226 | if not p["type"] then
227 | module:fail_json({msg="when creating sections, a type is required", message=message})
228 | end
229 |
230 | local message = {
231 | config=conf,
232 | name=path["section"],
233 | type=p["type"],
234 | }
235 |
236 | if path["option"] then
237 | message["values"]=values
238 | end
239 |
240 | res = module:ubus_call(conn, "uci", "add", message)
241 |
242 | elseif not compare_tables(target, query_value(module, conn, path, true)) then
243 | -- We have to take actions and use "uci set"
244 | local message = {
245 | config=conf,
246 | section=sec,
247 | values=values
248 | }
249 | res = module:ubus_call(conn, "uci", "set", message)
250 | else
251 | conn:close()
252 | module:exit_json({msg="Value already set", changed=false, result=res})
253 | end
254 |
255 |
256 | local autocommit = false
257 | if p["autocommit"] then
258 | autocommit = true
259 | docommit(module, conn, conf)
260 | end
261 |
262 | conn:close()
263 | module:exit_json({msg="Value successfully set", changed=true, autocommit=autocommit, result=res})
264 | end
265 |
266 | function unset_value(module)
267 | local p = module:get_params()
268 | local path = p["name"]
269 |
270 | local conn = module:ubus_connect()
271 |
272 | local conf, sec = check_config(module, conn, path["config"], path["section"])
273 |
274 | -- the whole section is already gone
275 | if nil == sec then
276 | -- already absent
277 | conn:close()
278 | module:exit_json({msg="Section already absent", changed=false})
279 | end
280 |
281 | -- and nil ~= sec...
282 | local message = {
283 | config=conf,
284 | section=sec
285 | }
286 |
287 | -- check if we have got a option
288 | if path["option"] then
289 | local is = query_value(module, conn, path, false)
290 | if not is then
291 | conn:close()
292 | module:exit_json({msg="Option already absent", changed=false})
293 | end
294 |
295 | message["option"] = path["option"]
296 | end
297 |
298 |
299 | local res = module:ubus_call(conn, "uci", "delete", message)
300 |
301 | local autocommit = false
302 | if p["autocommit"] then
303 | local autocommit = true
304 | docommit(module, conn, conf)
305 | end
306 |
307 | conn:close()
308 | module:exit_json({msg="Section successfully deleted", changed=true, autocommit=autocommit, result=res})
309 | end
310 |
311 | function check_parameters(module)
312 | local p = module:get_params()
313 |
314 | -- Validate the path
315 | if p["name"] then
316 | p["name"] = parse_path(module, p["name"])
317 | end
318 |
319 | -- op requires that no state is given, configs does not take any parameter
320 | if p["op"] then
321 | -- all operands do not take a state or value parameter
322 | if p["value"] then
323 | module:fail_json({msg="op=* do not work with 'state','value' or 'autocommit' arguments"})
324 | end
325 |
326 | -- config does not take a path parameter
327 | if "configs" == p["op"] and p["name"] then
328 | module:fail_json({msg="'op=config' does not take a 'path' argument"})
329 | end
330 | else
331 | -- in the normal case name and state are required
332 | if (not p["name"])
333 | or (not p["state"]) then
334 | module:fail_json({msg="Both name and state are required to set/unset values"})
335 | end
336 |
337 | -- when performing an "uci set", a value is required
338 | if ("set" == p["state"] or "present" == p["state"]) then
339 | if p["name"]["option"] and not p["value"] then -- Setting a regular value
340 | module:fail_json({msg="When using 'uci set', a value is required"})
341 | elseif not p["name"]["option"] and not p["type"] and not p["match"] then -- Creating a section
342 | module:fail_json({msg="When creating sections with 'uci set', a type is required"})
343 | end
344 | end
345 |
346 | if nil ~= p["value"] and ("unset" == p["state"] or "absent" == p["state"]) then
347 | module:fail_json({msg="When deleting options, no value can be set"})
348 | end
349 |
350 | if nil ~= p["forcelist"] and ("unset" == p["state"] or "absent" == p["state"]) then
351 | module:fail_json({msg="'forcelist' only applies to set operations"})
352 | end
353 | end
354 |
355 | end
356 |
357 | function main(arg)
358 | local module = Ansible.new({
359 | name = { aliases = {"path", "key"}, type="str"},
360 | value = { type="list" },
361 | state = { default="present", choices={"present", "absent", "set", "unset"} },
362 | op = { choices={"configs", "commit", "revert", "get"} },
363 | reload = { aliases = {"reload_configs", "reload-configs"}, type='bool'},
364 | autocommit = { default=true, type="bool" },
365 | forcelist = { default=false, type="bool" },
366 | type = { aliases = {"section-type"}, type="str" },
367 | socket = { type="path" },
368 | timeout = { type="int"},
369 | match = { type="dict"},
370 | values = { type="dict"}
371 | })
372 |
373 | module:parse(arg[1])
374 | check_parameters(module)
375 |
376 | local p = module:get_params()
377 |
378 | if p["reload"] then
379 | reload_configs(module)
380 | end
381 |
382 | -- Execute operation
383 | if "configs" == p["op"] then
384 | get_configs(module)
385 | elseif "commit" == p["op"] then
386 | commit(module)
387 | elseif "revert" == p["op"] then
388 | revert(module)
389 | elseif "get" == p["op"] then
390 | get(module)
391 | else
392 | -- If no op was given, simply enforce the setting state
393 | local state = p["state"]
394 | local doset = true
395 | if "absent" == state or "unset" == state then
396 | doset = false
397 | elseif "present" ~= state and "set" ~= state then
398 | module:fail_json({msg="Set state must be one of set, present, unset, absent"})
399 | end
400 |
401 | -- check if a full path was specified
402 | local path = p["name"]
403 | if not path["config"] then
404 | module:fail_json({msg="Set operation requires a path"})
405 | end
406 | if not path["section"] then
407 | if doset and not p["type"] and not p["match"] then
408 | module:fail_json({msg="Set operation requires a type, a match, or a path of"
409 | .. " the form '.[.