├── .gitignore
├── LICENSE
├── README.md
├── create-vms.sh
├── destroy-vms.sh
├── example-command.yml
├── example-exp2git.yml
├── example-mtfacts.yml
├── example-secure.yml
├── example-template.yml
├── example-upgrade.yml
├── library
├── mikrotik_command.py
├── mikrotik_export.py
├── mikrotik_facts.py
└── mikrotik_package.py
├── requirements.txt
└── routeros
├── archive.sh
├── latest.sh
└── update.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
91 | .vscode/
92 | test/
93 | test-*
94 | exports/*
95 | scripts/*
96 | templates/*
97 | routeros/*
98 | !routeros/update.sh
99 | !routeros/archive.sh
100 | !routeros/latest.sh
101 | *.retry
102 | *.vdi
103 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | {one line to give the program's name and a brief idea of what it does.}
635 | Copyright (C) {year} {name of author}
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | {project} Copyright (C) {year} {fullname}
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ansible-mikrotik
2 | [Ansible](https://www.ansible.com/) library for [MikroTik](https://mikrotik.com/) [RouterOS](https://mikrotik.com/software) network device management with python modules that can also be used in shell scripts. It was designed with following use-cases in mind:
3 | - [x] detailed device information (facts) gathering (**mikrotik_facts.py**),
4 | - [x] configuration backup and change management (**mikrotik_export.py**),
5 | - [x] RouterOS upgrades and package management (**mikrotik_package.py**),
6 | - [ ] direct command execution or script upload (**mikrotik_command.py**).
7 |
8 | Package management module works without internet access, however you need to create a local RouterOS package repository either manually or by using one of included shell scripts as described later in the 3rd step.
9 | ## 1. Basic prerequisites installation (Debian/Ubuntu):
10 | Install stable version of Ansible, for example by adding its [Launchpad](https://launchpad.net/~ansible/+archive/ubuntu/ansible) repository on ubuntu:
11 | ```sh
12 | sudo apt-add-repository ppa:ansible/ansible
13 | sudo apt update
14 | sudo apt install ansible git
15 | ```
16 | ## 2. Download the ansible-mikrotik library:
17 | ```sh
18 | git clone https://github.com/nekitamo/ansible-mikrotik.git
19 | cd ansible-mikrotik
20 | ```
21 | ## 3. Initialize local RouterOS package repository
22 | You can either use the following (more complicated) script which downloads less files (~550 MB) and creates includable ansible vars (versions.yml) with actual package versions for current and bugfix release trees:
23 | ```sh
24 | routeros/update.sh
25 | ```
26 | Or you can use this much simpler script that will download practically everything from MikroTik's latest software web page (1.5+ gigabytes):
27 | ```sh
28 | routeros/latest.sh
29 | ```
30 | Both scripts can be used at will to create proper directory structure for use with mikrotik_package.py module. Also, both will probably have to be constantly updated as MikroTik web pages evolve with time...
31 | ## 4. Run some tests to see if it works
32 | Running the included shell script 'create-vms.sh' should create a local test environment with 3 virtual MikroTik routers (aka CHRs). You can use them to run some example ansible playbooks like so:
33 | ```sh
34 | ./create-vms.sh
35 | ansible-playbook -i test-routers example-mtfacts.yml
36 | ansible-playbook -i test-routers example-exp2git.yml
37 | ansible-playbook -i test-routers example-upgrade.yml
38 | ```
39 | Try starting some of the playbooks multiple times and see what happens. There is also a cleanup script './destroy-vms.sh' which will shut down and delete virtual routers once you're done testing.
40 | ## 5. Some security considerations
41 | There are three basic ways you can handle ssh authentication:
42 | 1. **plaintext** passwords in playbooks/scripts or
43 | 2. passwords encrypted with **ansible vault** or
44 | 3. omit passwords and just use **ssh keys**.
45 |
46 | The included 'example-secure.yml' ansible playbook kind of walks you through all three. It starts with initial empty admin credentials to gain access to a 'blank' device, then sets new admin password from predefined credentials stored in encrypted ansible vault (test-vault.yml), and finally uploads admin's public ssh key which is later used instead of passwords.
47 | ## Shell mode usage (w/o ansible):
48 | Simply use `mikrotik_.py` modules from `/library` folder with shell command line options like so (ansible parameters and command line options are exactly the same):
49 | ```sh
50 | library/mikrotik_facts.py --hostname=192.168.88.101 --verbose
51 | ```
52 | Run it without arguments for basic usage info or open it with a text editor for detailed built-in ansible documentation.
53 | ## Useful tools - mactelnet
54 | This simple tool included in standard ubuntu repositories enables you to just plug a new MikroTik device into your management network and configure it for basic IP connectivity without WinBox.
55 | ```sh
56 | sudo apt install mactelnet-client
57 | mndp # or mactelnet -l, wait for device discovery and note the mac-address and port:
58 | #Searching for MikroTik routers... Abort with CTRL+C.
59 | #
60 | #IP MAC-Address Identity (platform version hardware) uptime
61 | #0.0.0.0 8:0:27:4e:f2:9b MikroTik (MikroTik 6.38.7 (bugfix) CHR) up 0 days 0 hours ether1
62 | #^C
63 | mactelnet -u admin -p '' # configure fixed ip address or dhcp-client via CLI:
64 | #[admin@MikroTik] > ip dhcp-client add interface=
65 | ```
66 | ## Troubleshooting (Debian/Ubuntu)
67 | ### SSH client error: No module named paramiko
68 | This means you need to install python's paramiko module. As simply apt-getting `python-paramiko` will probably just lead to problem described in the next chapter, run both commands at its end to get the latest version of paramiko right away.
69 | ### FutureWarning: CTR mode needs counter parameter, not IV
70 | If you see the above warning than your distribution's version of paramiko is, besides being pretty old, also broken and you should upgrade it:
71 | ```sh
72 | sudo apt install build-essential libssl-dev libffi-dev python-dev python-pip
73 | sudo -H pip install --upgrade paramiko
74 | ```
75 | #### Offline upgrade
76 | First, download everything you need into a new folder ("paramikopips") on a host with internet access:
77 | ```sh
78 | mkdir paramiko
79 | sudo -H pip download -r requirements.txt -d paramikopips
80 | ```
81 | Then transfer this folder to the off-line host and run:
82 | ```sh
83 | sudo -H pip install --no-index --find-links=paramikopips -r requirements.txt
84 | ```
85 | Naturally, for this to work the off-line host should already have previously mentioned distribution packages. But if you have hosts w/o internet access you've probably already figured out the need for some kind of apt-mirror or similar device...
--------------------------------------------------------------------------------
/create-vms.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | vdi="https://www.mikrotik.com/download"
5 | mgmt_lan="192.168.88"
6 | work_lan="10.0.0"
7 | test_dir=test
8 |
9 | [ -f test-routers ] && exit 1
10 | dpkg -s virtualbox > /dev/null || sudo apt install virtualbox # required
11 |
12 | mkdir -p $test_dir && cd $test_dir && mkdir -p VMs
13 | if [ $# -eq 0 ]; then # download latest vdi
14 | dl="https:$(wget -q -O- $vdi | grep -Po '(?<=a href=")[^"]*/routeros/[^"]*\.vdi[^"]*' | head -n1)"
15 | wget -nv -cN $dl
16 | chrhd="$(pwd)/$(basename $dl)"
17 | else # local vdi filename for offline use (./create-vms.sh )
18 | chrhd="$1"
19 | fi
20 | echo -e "CHR disk image: $chrhd\nSetting up VirtualBox networking..."
21 | VBoxManage list hostonlyifs | grep -q vboxnet0 || VBoxManage hostonlyif create #vboxnet0
22 | VBoxManage hostonlyif ipconfig vboxnet0 --ip $mgmt_lan.254 # management network
23 | VBoxManage list dhcpservers | grep -q vboxnet0 || VBoxManage dhcpserver add --ifname vboxnet0 \
24 | --ip $mgmt_lan.254 --netmask 255.255.255.0 --lowerip $mgmt_lan.101 --upperip $mgmt_lan.200
25 | VBoxManage dhcpserver modify --ifname vboxnet0 --enable
26 | VBoxManage list hostonlyifs | grep -q vboxnet1 || VBoxManage hostonlyif create #vboxnet1
27 | VBoxManage hostonlyif ipconfig vboxnet1 --ip $work_lan.1
28 | echo "[mikrotik_routers]" > ../test-routers
29 | for vm in {1..3}; do
30 | chrvm="chr$vm"; echo
31 | VBoxManage createvm --name $chrvm --ostype "Other_64" --basefolder $(pwd)/VMs --register
32 | VBoxManage createhd --filename $(pwd)/VMs/$chrvm/$chrvm-hd.vdi --diffparent $chrhd
33 | VBoxManage storagectl $chrvm --name "SATA1" --add sata --controller IntelAHCI
34 | VBoxManage storageattach $chrvm --storagectl "SATA1" --port 0 --device 0 --type hdd \
35 | --medium $(pwd)/VMs/$chrvm/$chrvm-hd.vdi
36 | VBoxManage modifyvm $chrvm --memory 128 --boot1 disk --boot2 none --boot3 none --boot4 none
37 | VBoxManage modifyvm $chrvm --nic1 hostonly --nictype1 virtio --hostonlyadapter1 vboxnet0
38 | VBoxManage modifyvm $chrvm --nic2 hostonly --nictype2 virtio --hostonlyadapter2 vboxnet1 \
39 | --nicpromisc2 allow-all
40 | VBoxManage modifyvm $chrvm --nic3 nat --nictype3 virtio --cableconnected3 off
41 | VBoxManage startvm $chrvm --type headless > /dev/null
42 | echo -n "Waiting for '$chrvm' to start: "
43 | for up in {1..60}; do
44 | echo -n "."
45 | ping -qnc 1 $mgmt_lan.10$vm > /dev/null && break
46 | done
47 | if [ "$up" -eq "60" ]; then
48 | echo -e " TIMEOUT!\nNo response from $chrvm, deployment aborted."; exit 1
49 | fi
50 | echo " OK."
51 | echo "$mgmt_lan.10$vm sys_id=$chrvm" >> ../test-routers
52 | done
53 |
54 | exit 0
55 | test_key=test-rsa
56 | test_vault=test-vault.yml
57 | test_vpf=test-password
58 | test_password="test"
59 | dpkg -s pwgen > /dev/null || sudo apt install pwgen # required
60 | echo -n "Generating test keys and password... "
61 | echo "$test_password" > $test_vpf
62 | echo "private_key: |" > $test_vault
63 | ssh-keygen -qf $test_key -t rsa -N '' || exit 1
64 | while read ln; do
65 | echo " $ln" >> $test_vault
66 | done <$test_key
67 | echo "admin_password: $(pwgen -H $test_key -s 10 1)" >> $test_vault
68 | ansible-vault encrypt --vault-password-file=$test_vpf $test_vault
69 |
--------------------------------------------------------------------------------
/destroy-vms.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | test_dir=test
5 |
6 | [ -f test-routers ] || exit 1
7 | read -p "Press [enter] to delete test VMs or [Ctrl-C] to abort..."
8 | VBoxManage list runningvms | cut -d\" -f2 | grep chr |
9 | while read vm; do
10 | VBoxManage controlvm $vm poweroff
11 | done
12 | VBoxManage list vms | cut -d\" -f2 | grep chr |
13 | while read vm; do
14 | VBoxManage unregistervm $vm
15 | rm -rf $test_dir/VMs/$vm
16 | done
17 | VBoxManage dhcpserver remove -ifname vboxnet0
18 | VBoxManage hostonlyif remove vboxnet0
19 | VBoxManage hostonlyif remove vboxnet1
20 | rm -f test-*
21 | #rm -rf $test_dir
22 |
23 | exit 0
--------------------------------------------------------------------------------
/example-command.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: execute MikroTik CLI command
4 | hosts: mikrotik_routers
5 | connection: local
6 |
7 | tasks:
8 |
9 | - name: gather facts from routers
10 | mikrotik_command:
11 | hostname: "{{ inventory_hostname }}"
12 | username: admin
13 | command: 'system license print'
14 | register: cmdout
15 |
16 | - debug:
17 | var: cmdout.stdout_lines
18 |
--------------------------------------------------------------------------------
/example-exp2git.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: Export and commit to git
4 | hosts: mikrotik_routers
5 | gather_facts: no
6 | connection: local
7 |
8 | vars:
9 | export_dir: exports
10 |
11 | tasks:
12 |
13 | - name: Check git repository
14 | command: git init
15 | args:
16 | chdir: "{{ export_dir }}"
17 | creates: ".git"
18 | run_once: true
19 |
20 | - name: MikroTik configuration export
21 | mikrotik_export:
22 | hostname: "{{ inventory_hostname }}"
23 | username: admin
24 | export_dir: "{{ export_dir }}"
25 | hide_sensitive: true
26 | timestamp: false
27 |
28 | - name: Git add new/modified/deleted and commit
29 | shell: git add --all && git commit -m "ansible commit"
30 | args:
31 | chdir: "{{ export_dir }}"
32 | register: commit
33 | failed_when: commit.stderr
34 | changed_when: not commit.rc
35 | run_once: true
36 |
37 | - name: Git push to remote repo
38 | command: git push
39 | args:
40 | chdir: "{{ export_dir }}"
41 | register: result
42 | failed_when: result.rc
43 | changed_when: '"up-to-date" not in result.stderr'
44 | when: commit.changed and git_remote is defined
45 | run_once: true
46 |
--------------------------------------------------------------------------------
/example-mtfacts.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: collect MikroTik facts
4 | hosts: mikrotik_routers
5 | gather_facts: no
6 | connection: local
7 |
8 | tasks:
9 |
10 | - name: gather facts from routers
11 | mikrotik_facts:
12 | hostname: "{{ inventory_hostname }}"
13 | username: admin
14 | verbose: yes
15 | register: mikrotik
16 |
17 | - debug:
18 | var: mikrotik
19 |
--------------------------------------------------------------------------------
/example-secure.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: execute MikroTik CLI command
4 | hosts: mikrotik_routers
5 | gather_facts: no
6 | connection: local
7 |
8 | vars:
9 | test_key: test-id_rsa
10 | test_vault: test-vault.yml
11 | test_vpf: test-password
12 | test_password: test
13 |
14 | tasks:
15 | - block:
16 |
17 | - name: create vault password file
18 | shell: 'echo {{ test_password }} > {{ test_vpf }}'
19 | args:
20 | creates: '{{ test_vpf }}'
21 |
22 | - name: adjust vault password file permissions
23 | file:
24 | path: '{{ test_vpf }}'
25 | mode: 0600
26 |
27 | - name: generate test ssh keys
28 | shell: "ssh-keygen -qf {{ test_key }} -t rsa -N '' -C test_key"
29 | args:
30 | creates: '{{ test_key }}'
31 |
32 | - name: test if pwgen is installed
33 | command: which pwgen
34 |
35 | - name: create and encrypt the test vault
36 | shell: |
37 | echo "admin_password: $(pwgen -H {{ test_key }} -s 10 1)" >>{{ test_vault }} &&
38 | ansible-vault encrypt --vault-password-file={{ test_vpf }} {{ test_vault }}
39 | args:
40 | creates: '{{ test_vault }}'
41 |
42 | run_once: yes
43 |
44 | # - pause:
45 |
46 | - name: gather facts from routers
47 | mikrotik_facts:
48 | hostname: "{{ inventory_hostname }}"
49 | username: admin
50 | key_filename: "{{ test_key }}"
51 | register: mikrotik
52 |
53 | - debug:
54 | var: mikrotik.user_ssh_keys
55 | when: (mikrotik.user_ssh_keys is not defined) or
56 | ("'test_key' not in mikrotik.user_ssh_keys")
57 |
--------------------------------------------------------------------------------
/example-template.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: MikroTik RouterOS management
4 | hosts: mikrotik_routers
5 | gather_facts: no
6 | connection: local
7 | #serial: 1
8 |
9 | vars:
10 | username: "admin"
11 | password: ""
12 | primary_ntp: 192.168.88.100
13 | secondary_ntp: 192.168.0.1
14 | snmp_community: "mt_mgmt"
15 | snmp_addresses: 192.168.88.0/24
16 | snmp_server: 192.168.88.100
17 | snmp_trapserver: 192.168.88.100
18 | syslog_server: 192.168.88.100
19 | syslog_port: 1514
20 | syslog_topics: "!debug,!packet,!snmp"
21 |
22 | tasks:
23 |
24 | - name: Gather device facts
25 | mikrotik_facts:
26 | hostname: "{{ inventory_hostname }}"
27 | username: "{{ username }}"
28 | password: "{{ password }}"
29 |
30 | - name: Prepare default setup from template
31 | template:
32 | src: defaults.rsc.j2
33 | dest: "scripts/{{ inventory_hostname }}-defaults.rsc"
34 | register: defaults
35 |
36 | - pause:
37 |
38 | - name: Apply default setup on device(s)
39 | mikrotik_command:
40 | command: "scripts/{{ inventory_hostname }}-defaults.rsc"
41 | run_block: true
42 | hostname: "{{ inventory_hostname }}"
43 | username: "{{ username }}"
44 | password: "{{ password }}"
45 | when: defaults.changed
46 |
--------------------------------------------------------------------------------
/example-upgrade.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | - name: Upgrade MikroTik routers
4 | hosts: mikrotik_routers
5 | gather_facts: no
6 | connection: local
7 | #serial: 1
8 |
9 | vars_files:
10 | - routeros/versions.yml
11 |
12 | tasks:
13 |
14 | - debug:
15 | var: routeros_current
16 | run_once: yes
17 |
18 | - name: upgrade and reboot
19 | mikrotik_package:
20 | hostname: "{{ inventory_hostname }}"
21 | username: admin
22 | version: "{{ routeros_current }}"
23 | packages:
24 | - system
25 | - security
26 | - dhcp
27 | - advanced-tools
28 | reboot: true
29 | register: result
30 |
31 | - debug:
32 | var: result
33 |
--------------------------------------------------------------------------------
/library/mikrotik_command.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | """MikroTik RouterOS CLI ansible module"""
4 |
5 | import os
6 | import sys
7 | import socket
8 |
9 | try:
10 | HAS_SSHCLIENT = True
11 | import paramiko
12 | except ImportError as import_error:
13 | HAS_SSHCLIENT = False
14 | try:
15 | SHELLMODE = False
16 | from ansible.module_utils.basic import AnsibleModule
17 | except ImportError:
18 | SHELLMODE = True
19 | else:
20 | if sys.stdin.isatty():
21 | SHELLMODE = True
22 |
23 | SHELLDEFS = {
24 | 'username': 'admin',
25 | 'password': '',
26 | 'key_filename': None,
27 | 'timeout': 30,
28 | 'port': 22,
29 | 'test_change': True,
30 | 'command': None,
31 | 'execute_file': None,
32 | 'upload_script': None,
33 | 'upload_file': None
34 | }
35 | MIKROTIK_MODULE = '[github.com/nekitamo/ansible-mikrotik] v17.07'
36 | DOCUMENTATION = """
37 | ---
38 | module: mikrotik_command
39 | short_description: Execute single or multiple MikroTik RouterOS CLI commands
40 | description:
41 | - Execute one or more MikroTik RouterOS CLI commands via ansible or shell
42 | - Execute multiple commands from a file or save them as a RouterOS script
43 | - Authenticate via username/password or by using ssh keys
44 | return_data:
45 | - changed
46 | - stdout
47 | - stdout_lines
48 | options:
49 | command:
50 | description:
51 | - MikroTik command to execute on the device
52 | required: false
53 | default: null
54 | hostname:
55 | description:
56 | - IP Address or hostname of the MikroTik device
57 | required: true
58 | default: null
59 | execute_file:
60 | description:
61 | - Execute multiple commands from the specified file
62 | required: no
63 | choices: true, false
64 | default: false
65 | upload_script:
66 | description:
67 | - Upload commands from file specified in command and save as a script
68 | required: no
69 | default: false
70 | test_change:
71 | description:
72 | - Test for configuration changes after command execution (slow)
73 | required: no
74 | default: false
75 | upload_file:
76 | description:
77 | - Upload specified file before command/script execution
78 | required: no
79 | default: null
80 | port:
81 | description:
82 | - SSH listening port of the MikroTik device
83 | required: no
84 | default: 22
85 | username:
86 | description:
87 | - Username used to login for the device
88 | required: no
89 | default: ansible
90 | password:
91 | description:
92 | - Password used to login to the device
93 | required: no
94 | default: null
95 | """
96 | EXAMPLES = """
97 | - name: Upload and assign ssh key
98 | mikrotik_command:
99 | hostname: "{{ inventory_hostname }}"
100 | username: ansible
101 | password: ""
102 | upload_file: "~/.ssh/id_rsa.pub"
103 | command: "/user ssh-keys import public-key-file=id_rsa.pub user=ansible"
104 | - name: Reboot router
105 | mikrotik_command:
106 | hostname: "{{ inventory_hostname }}"
107 | command: "system reboot"
108 | """
109 | RETURN = """
110 | stdout:
111 | description: Returns router response in a single string
112 | returned: always
113 | type: string
114 | stdout_lines:
115 | description: Returns router response as a list of strings
116 | returned: always
117 | type: list
118 | """
119 | SHELL_USAGE = """
120 | mikrotik_command.py --hostname= --command=
121 | [--run_block] [--upload_script] [--upload_file=]
122 | [--port=] [--username=] [--password=]
123 | """
124 |
125 | def safe_fail(module, device=None, **kwargs):
126 | """closes device before module fail"""
127 | if device:
128 | device.close()
129 | module.fail_json(**kwargs)
130 |
131 | def safe_exit(module, device=None, **kwargs):
132 | """closes device before module exit"""
133 | if device:
134 | device.close()
135 | module.exit_json(**kwargs)
136 |
137 | def parse_opts(cmdline):
138 | """returns SHELLMODE command line options as dict"""
139 | options = SHELLDEFS
140 | for opt in cmdline:
141 | if opt.startswith('--'):
142 | try:
143 | arg, val = opt.split("=", 1)
144 | except ValueError:
145 | arg = opt
146 | val = True
147 | else:
148 | if val.lower() in ('no', 'false', '0'):
149 | val = False
150 | elif val.lower() in ('yes', 'true', '1'):
151 | val = True
152 | arg = arg[2:]
153 | if arg in options or arg == 'hostname':
154 | options[arg] = val
155 | else:
156 | print SHELL_USAGE
157 | sys.exit("Unknown option: --%s" % arg)
158 | if 'hostname' not in options:
159 | print SHELL_USAGE
160 | sys.exit("Hostname is required, specify with --hostname=")
161 | return options
162 |
163 | def device_connect(module, device, rosdev):
164 | """open ssh connection with or without ssh keys"""
165 | if SHELLMODE:
166 | sys.stdout.write("Opening SSH connection to %s(%s:%s)... "
167 | % (rosdev['hostname'], rosdev['ipaddress'], rosdev['port']))
168 | sys.stdout.flush()
169 | try:
170 | device.connect(rosdev['ipaddress'], username=rosdev['username'],
171 | password=rosdev['password'], port=rosdev['port'],
172 | timeout=rosdev['timeout'])
173 | except Exception:
174 | try:
175 | device.connect(rosdev['ipaddress'], username=rosdev['username'],
176 | password=rosdev['password'], port=rosdev['port'],
177 | timeout=rosdev['timeout'], allow_agent=False,
178 | look_for_keys=False)
179 | except Exception as ssh_error:
180 | if SHELLMODE:
181 | sys.exit("failed!\nSSH error: " + str(ssh_error))
182 | safe_fail(module, device, msg=str(ssh_error),
183 | description='error opening ssh connection to %s(%s:%s)' %
184 | (rosdev['hostname'], rosdev['ipaddress'], rosdev['port']))
185 | if SHELLMODE:
186 | print "succes."
187 |
188 | def sshcmd(module, device, timeout, command):
189 | """executes a command on the device, returns string"""
190 | try:
191 | _stdin, stdout, _stderr = device.exec_command(command, timeout=timeout)
192 | except Exception as ssh_error:
193 | if SHELLMODE:
194 | sys.exit("SSH command error: " + str(ssh_error))
195 | safe_fail(module, device, msg=str(ssh_error),
196 | description='SSH error while executing command')
197 | response = stdout.read()
198 | if 'bad command name ' not in response:
199 | if 'syntax error ' not in response:
200 | if 'failure: ' not in response:
201 | return response.rstrip()
202 | if SHELLMODE:
203 | print "Command: " + str(command)
204 | sys.exit("Error: " + str(response))
205 | safe_fail(module, device, msg=str(ssh_error),
206 | description='bad command name or syntax error')
207 |
208 | def main():
209 | """RouterOS command line interface main"""
210 | rosdev = {}
211 | cmd_timeout = 30
212 | changed = True
213 | if not SHELLMODE:
214 | module = AnsibleModule(
215 | argument_spec=dict(
216 | command=dict(default=None, type='str'),
217 | execute_file=dict(default=None, type='path'),
218 | upload_script=dict(default=None, type='path'),
219 | test_change=dict(default=True, type='bool'),
220 | upload_file=dict(default=None, type='path'),
221 | key_filename=dict(default=None, type='path'),
222 | port=dict(default=22, type='int'),
223 | timeout=dict(default=30, type='float'),
224 | hostname=dict(required=True, type='str'),
225 | username=dict(default='ansible', type='str'),
226 | password=dict(default=None, type='str', no_log=True),
227 | ), supports_check_mode=False
228 | )
229 | if not HAS_SSHCLIENT:
230 | safe_fail(module, msg='There was a problem loading module: ',
231 | error=str(import_error))
232 | command = module.params['command']
233 | execute_file = module.params['run_block']
234 | upload_script = module.params['upload_script']
235 | test_change = module.params['test_change']
236 | upload_file = module.params['upload_file']
237 | rosdev['key_filename'] = module.params['key_filename']
238 | rosdev['hostname'] = module.params['hostname']
239 | rosdev['username'] = module.params['username']
240 | rosdev['password'] = module.params['password']
241 | rosdev['port'] = module.params['port']
242 | rosdev['timeout'] = module.params['timeout']
243 |
244 | else:
245 | if not HAS_SSHCLIENT:
246 | sys.exit("SSH client error: " + str(import_error))
247 | if not SHELLOPTS['command']:
248 | print SHELL_USAGE
249 | sys.exit("command required, specify with --command=")
250 | rosdev['hostname'] = SHELLOPTS['hostname']
251 | rosdev['username'] = SHELLOPTS['username']
252 | rosdev['password'] = SHELLOPTS['password']
253 | rosdev['port'] = SHELLOPTS['port']
254 | rosdev['timeout'] = SHELLOPTS['timeout']
255 | rosdev['key_filename'] = SHELLOPTS['key_filename']
256 | command = SHELLOPTS['command']
257 | execute_file = SHELLOPTS['execute_file']
258 | upload_script = SHELLOPTS['upload_script']
259 | test_change = SHELLOPTS['test_change']
260 | upload_file = SHELLOPTS['upload_file']
261 | module = None
262 |
263 | try:
264 | rosdev['ipaddress'] = socket.gethostbyname(rosdev['hostname'])
265 | except socket.gaierror as dns_error:
266 | if SHELLMODE:
267 | sys.exit("Hostname error: " + str(dns_error))
268 | safe_fail(module, msg=str(dns_error),
269 | description='error getting device address from hostname')
270 |
271 | device = paramiko.SSHClient()
272 | device.set_missing_host_key_policy(paramiko.AutoAddPolicy())
273 | device_connect(module, device, rosdev)
274 |
275 | if test_change:
276 | before = sshcmd(module, device, cmd_timeout, "export")
277 |
278 | if upload_file and os.path.isfile(upload_file):
279 | uploaded = os.path.basename(upload_file)
280 | sftp = device.open_sftp()
281 | sftp.put(upload_file, uploaded)
282 | sftp.close()
283 | response = sshcmd(module, device, cmd_timeout,
284 | 'file print terse without-paging where name="'
285 | + uploaded + '"')
286 | if uploaded not in response:
287 | if SHELLMODE:
288 | device.close()
289 | sys.exit("Error uploading file: " + uploaded)
290 | safe_fail(module, device, msg="upload failed!",
291 | description='error uploading file: ' + uploaded)
292 |
293 | if upload_script and os.path.isfile(upload_script):
294 | response = ''
295 | try:
296 | with open(upload_script) as scriptfile:
297 | script = scriptfile.readlines()
298 | scriptfile.close()
299 | except Exception as cmd_error:
300 | if SHELLMODE:
301 | device.close()
302 | sys.exit("Script file error: " + str(cmd_error))
303 | safe_fail(module, device, msg=str(cmd_error),
304 | description='error opening script file')
305 | scriptname = os.path.basename(upload_script)
306 | response += sshcmd(module, device, cmd_timeout,
307 | '/system script remove [ find name="'
308 | + scriptname + '" ]')
309 | cmd = '/system script add name="' + scriptname + '" source="'
310 | for line in script:
311 | line = line.rstrip()
312 | line = line.replace("\\", "\\\\")
313 | line = line.replace("\"", "\\\"")
314 | line = line.replace("$", "\\$")
315 | cmd += line + "\\r\\n"
316 | response += sshcmd(module, device, cmd_timeout, cmd + '"')
317 | elif run_block:
318 | for cmd in script:
319 | if cmd[0] != "#":
320 | rsp = sshcmd(module, device, cmd_timeout, cmd)
321 | if rsp:
322 | response += rsp + '\r\n'
323 | else:
324 | if upload_file and command == 'user ssh-keys import':
325 | response = sshcmd(module, device, cmd_timeout,
326 | '/user ssh-keys import public-key-file="' +
327 | uploaded + '" user=' + rosdev['username'])
328 | else:
329 | response = sshcmd(module, device, cmd_timeout, command)
330 | if response:
331 | response += '\r\n'
332 |
333 | if test_change:
334 | after = sshcmd(module, device, cmd_timeout, "/export")
335 | before = before.splitlines(1)[1:]
336 | after = after.splitlines(1)[1:]
337 | if len(before) == len(after):
338 | for bef, aft in zip(before, after):
339 | if aft != bef:
340 | break
341 | else:
342 | changed = False
343 |
344 | if SHELLMODE:
345 | device.close()
346 | print str(response)
347 | sys.exit(0)
348 |
349 | stdout_lines = []
350 | for line in response.splitlines():
351 | if line:
352 | stdout_lines.append(line.strip())
353 |
354 | safe_exit(module, device, stdout=response, stdout_lines=stdout_lines,
355 | changed=changed)
356 |
357 | if __name__ == '__main__':
358 | if len(sys.argv) > 1 or SHELLMODE:
359 | print "Ansible MikroTik Library %s" % MIKROTIK_MODULE
360 | SHELLOPTS = parse_opts(sys.argv)
361 | SHELLMODE = True
362 | main()
363 |
--------------------------------------------------------------------------------
/library/mikrotik_export.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | """MikroTik RouterOS backup and change manager"""
4 |
5 | import sys
6 | import re
7 | import socket
8 | import os
9 |
10 | HAS_SSHCLIENT = True
11 | SHELLMODE = False
12 | SHELLDEFS = {
13 | 'username': 'admin',
14 | 'password': '',
15 | 'timeout': 30,
16 | 'port': 22,
17 | 'export_dir': None,
18 | 'export_file' : None,
19 | 'backup_dir': None,
20 | 'timestamp': False,
21 | 'hide_sensitive': True,
22 | 'local_file': False,
23 | 'verbose': False
24 | }
25 | MIKROTIK_MODULE = '[github.com/nekitamo/ansible-mikrotik] v2017.03.28'
26 | DOCUMENTATION = """
27 | ---
28 |
29 | module: mikrotik_export
30 | short_description: MikroTik RouterOS configuration export
31 | description:
32 | - Exports full router configuration to a text file in export directory
33 | - By default no local export file is created on the router (enable with local_file: yes)
34 | - If you create router user 'ansible' with ssh-key you can omit username/password in playbooks
35 | return_data:
36 | - identity
37 | - software_id
38 | - export_dir
39 | - export_file
40 | - backup_dir
41 | - backup_files
42 | options:
43 | export_dir:
44 | description:
45 | - Directory where export_file is written after export, created if not existing
46 | required: true
47 | default: null
48 | export_file:
49 | description:
50 | - The name of the exported file, existing files are overwritten
51 | required: false
52 | default: _.rsc
53 | backup_dir:
54 | description:
55 | - Directory where all backups are downloaded, existing files are not overwritten
56 | required: false
57 | default: null
58 | timestamp:
59 | description:
60 | - Leave default timestamp in export file (first line), disabled for version tracking
61 | required: false
62 | default: false
63 | hide_sensitive:
64 | description:
65 | - Do not include passwords or other sensitive info in exported configuration file
66 | required: false
67 | default: true
68 | local_file:
69 | description:
70 | - Also write config as local file on device (ansible-export.rsc) before export
71 | - Exports via local_file option allways include timestamp in first line
72 | required: false
73 | default: false
74 | verbose:
75 | description:
76 | - Export verbose config including default option values (large export file)
77 | required: false
78 | default: false
79 | port:
80 | description:
81 | - SSH listening port of the MikroTik device
82 | required: false
83 | default: 22
84 | hostname:
85 | description:
86 | - IP Address or hostname of the MikroTik device
87 | required: true
88 | default: null
89 | username:
90 | description:
91 | - Username used to login to the device
92 | required: false
93 | default: ansible
94 | password:
95 | description:
96 | - Password used to login to the device
97 | required: false
98 | default: null
99 |
100 | """
101 | EXAMPLES = """
102 | # example playbook
103 | ---
104 |
105 | - name: Export Mikrotik RouterOS config
106 | hosts: mikrotik_routers
107 | gather_facts: false
108 | connection: local
109 |
110 | tasks:
111 |
112 | - name: Export router configurations
113 | mikrotik_export:
114 | hostname: "{{ inventory_hostname }}"
115 | export_dir: exports
116 | hide_sensitive: false
117 | timestamp: true
118 | """
119 | RETURN = """
120 | identity:
121 | description: Returns device identity (system identity print)
122 | returned: always
123 | type: string
124 | software_id:
125 | description: Returns device software_id (system identity print)
126 | returned: always
127 | type: string
128 | export_dir:
129 | description: Returns full os path for export directory
130 | returned: always
131 | type: string
132 | export_file:
133 | description: Returns filename of exported configuration
134 | returned: always
135 | type: string
136 | backup_dir:
137 | description: Returns full os path where backups were downloaded
138 | returned: if backup_dir option was used
139 | type: string
140 | backup_files:
141 | description: Returns list of downloaded backups
142 | returned: if backup_dir option was used
143 | type: list
144 | """
145 | SHELL_USAGE = """
146 | mikrotik_export.py --hostname= --export_dir=
147 | [--export_file=] [--backup_dir=]
148 | [--timestamp] [--hide_sensitive=no] [--verbose]
149 | [--local_file] [--timeout=] [--port=]
150 | [--username=] [--password=]
151 | """
152 |
153 | try:
154 | import paramiko
155 | except ImportError as import_error:
156 | HAS_SSHCLIENT = False
157 |
158 | try:
159 | from ansible.module_utils.basic import AnsibleModule
160 | except ImportError:
161 | SHELLMODE = True
162 | else:
163 | # ansible parameters on stdin?
164 | if sys.stdin.isatty():
165 | SHELLMODE = True
166 |
167 | def safe_fail(module, device=None, **kwargs):
168 | """closes device before module fail"""
169 | if device:
170 | device.close()
171 | module.fail_json(**kwargs)
172 |
173 | def safe_exit(module, device=None, **kwargs):
174 | """closes device before module exit"""
175 | if device:
176 | device.close()
177 | module.exit_json(**kwargs)
178 |
179 | def parse_opts(cmdline):
180 | """returns SHELLMODE command line options as dict"""
181 | options = SHELLDEFS
182 | for opt in cmdline:
183 | if opt.startswith('--'):
184 | try:
185 | arg, val = opt.split("=", 1)
186 | except ValueError:
187 | arg = opt
188 | val = True
189 | else:
190 | if val.lower() in ('no', 'false', '0'):
191 | val = False
192 | elif val.lower() in ('yes', 'true', '1'):
193 | val = True
194 | arg = arg[2:]
195 | if arg in options or arg == 'hostname':
196 | options[arg] = val
197 | else:
198 | print SHELL_USAGE
199 | sys.exit("Unknown option: --%s" % arg)
200 | if 'hostname' not in options:
201 | print SHELL_USAGE
202 | sys.exit("Hostname is required, specify with --hostname=")
203 | return options
204 |
205 | def device_connect(module, device, rosdev):
206 | """open ssh connection with or without ssh keys"""
207 | try:
208 | rosdev['hostname'] = socket.gethostbyname(rosdev['hostname'])
209 | except socket.gaierror as dns_error:
210 | if SHELLMODE:
211 | sys.exit("Hostname error: " + str(dns_error))
212 | safe_fail(module, device, msg=str(dns_error),
213 | description='error getting device address from hostname')
214 | if SHELLMODE:
215 | sys.stdout.write("Opening SSH connection to %s:%s... "
216 | % (rosdev['hostname'], rosdev['port']))
217 | sys.stdout.flush()
218 | try:
219 | device.connect(rosdev['hostname'], username=rosdev['username'],
220 | password=rosdev['password'], port=rosdev['port'],
221 | timeout=rosdev['timeout'])
222 | except Exception:
223 | try:
224 | device.connect(rosdev['hostname'], username=rosdev['username'],
225 | password=rosdev['password'], port=rosdev['port'],
226 | timeout=rosdev['timeout'], allow_agent=False,
227 | look_for_keys=False)
228 | except Exception as ssh_error:
229 | if SHELLMODE:
230 | sys.exit("failed!\nSSH error: " + str(ssh_error))
231 | safe_fail(module, device, msg=str(ssh_error),
232 | description='error opening ssh connection to %s' % rosdev['hostname'])
233 | if SHELLMODE:
234 | print "succes."
235 |
236 | def sshcmd(module, device, timeout, command):
237 | """executes a command on the device, returns string"""
238 | try:
239 | _stdin, stdout, _stderr = device.exec_command(command, timeout=timeout)
240 | except Exception as ssh_error:
241 | if SHELLMODE:
242 | sys.exit("SSH command error: " + str(ssh_error))
243 | safe_fail(module, device, msg=str(ssh_error),
244 | description='SSH error while executing command')
245 | response = stdout.read()
246 | if 'bad command name ' not in response:
247 | if 'syntax error ' not in response:
248 | if 'failure: ' not in response:
249 | return response.rstrip()
250 | if SHELLMODE:
251 | print "Command: " + str(command)
252 | sys.exit("Error: " + str(response))
253 | safe_fail(module, device, msg=str(ssh_error),
254 | description='bad command name or syntax error')
255 |
256 | def parse_terse(device, key, command):
257 | """executes a command and returns list"""
258 | _stdin, stdout, _stderr = device.exec_command(command)
259 | vals = []
260 | for line in stdout.readlines():
261 | if key in line:
262 | val = line.split(key+'=')[1]
263 | vals.append(val.split(' ')[0])
264 | return vals
265 |
266 | def parse_facts(device, command, pfx=""):
267 | """executes a command and returns dict"""
268 | _stdin, stdout, _stderr = device.exec_command(command)
269 | facts = {}
270 | for line in stdout.readlines():
271 | if ':' in line:
272 | fact, value = line.partition(":")[::2]
273 | fact = fact.replace('-', '_')
274 | if pfx not in fact:
275 | facts[pfx + fact.strip()] = str(value.strip())
276 | else:
277 | facts[fact.strip()] = str(value.strip())
278 | return facts
279 |
280 | def vercmp(ver1, ver2):
281 | """quick and dirty version comparison from stackoverflow"""
282 | def normalize(ver):
283 | return [int(x) for x in re.sub(r'(\.0+)*$', '', ver).split(".")]
284 | return cmp(normalize(ver1), normalize(ver2))
285 |
286 | def main():
287 | rosdev = {}
288 | backup_files = []
289 | cmd_timeout = 30
290 | changed = False
291 | if not SHELLMODE:
292 | module = AnsibleModule(
293 | argument_spec=dict(
294 | export_dir=dict(required=True, type='path'),
295 | export_file=dict(required=False, type='str'),
296 | backup_dir=dict(required=False, type='path'),
297 | timestamp=dict(default=False, type='bool'),
298 | hide_sensitive=dict(default=True, type='bool'),
299 | local_file=dict(default=False, type='bool'),
300 | verbose=dict(default=False, type='bool'),
301 | hostname=dict(required=True),
302 | username=dict(default='ansible', type='str'),
303 | password=dict(default='', type='str', no_log=True),
304 | port=dict(default=22, type='int'),
305 | timeout=dict(default=30, type='float')
306 | ), supports_check_mode=False
307 | )
308 | if not HAS_SSHCLIENT:
309 | safe_fail(module, msg='There was a problem loading module: ',
310 | error=str(import_error))
311 | export_dir = os.path.expanduser(module.params['export_dir'])
312 | export_file = module.params['export_file']
313 | backup_dir = module.params['backup_dir']
314 | timestamp = module.params['timestamp']
315 | hide_sensitive = module.params['hide_sensitive']
316 | local_file = module.params['local_file']
317 | verbose = module.params['verbose']
318 | rosdev['hostname'] = module.params['hostname']
319 | rosdev['username'] = module.params['username']
320 | rosdev['password'] = module.params['password']
321 | rosdev['port'] = module.params['port']
322 | rosdev['timeout'] = module.params['timeout']
323 |
324 | else:
325 | if not HAS_SSHCLIENT:
326 | sys.exit("SSH client error: " + str(import_error))
327 | if not SHELLOPTS['export_dir']:
328 | print SHELL_USAGE
329 | sys.exit("export_dir required, specify with --export_dir=")
330 | export_dir = os.path.expanduser(SHELLOPTS['export_dir'])
331 | rosdev['hostname'] = SHELLOPTS['hostname']
332 | rosdev['username'] = SHELLOPTS['username']
333 | rosdev['password'] = SHELLOPTS['password']
334 | rosdev['port'] = SHELLOPTS['port']
335 | rosdev['timeout'] = SHELLOPTS['timeout']
336 | hide_sensitive = SHELLOPTS['hide_sensitive']
337 | export_file = SHELLOPTS['export_file']
338 | backup_dir = SHELLOPTS['backup_dir']
339 | timestamp = SHELLOPTS['timestamp']
340 | local_file = SHELLOPTS['local_file']
341 | verbose = SHELLOPTS['verbose']
342 | module = None
343 |
344 | export_dir = os.path.realpath(export_dir)
345 | if not os.path.exists(export_dir):
346 | try:
347 | os.mkdir(export_dir, 0775)
348 | except OSError as mkdir_error:
349 | if SHELLMODE:
350 | sys.exit("Export directory error: " + str(mkdir_error))
351 | safe_fail(module, msg=str(mkdir_error),
352 | description='error creating export directory')
353 |
354 | device = paramiko.SSHClient()
355 | device.set_missing_host_key_policy(paramiko.AutoAddPolicy())
356 | device_connect(module, device, rosdev)
357 |
358 | version = sshcmd(module, device, cmd_timeout,
359 | ":put [/system resource get version]")
360 | response = sshcmd(module, device, cmd_timeout, "system identity print")
361 | identity = str(response.split(": ")[1])
362 | identity = identity.strip()
363 | software_id = sshcmd(module, device, cmd_timeout,
364 | ":put [ /system license get software-id ]")
365 | if not software_id:
366 | software_id = rosdev['hostname']
367 | if not export_file:
368 | export_file = identity + "_" + software_id + ".rsc"
369 | exportfull = os.path.join(export_dir, export_file)
370 | exportcmd = "export"
371 | if hide_sensitive:
372 | exportcmd += " hide-sensitive"
373 | if verbose:
374 | exportcmd += " verbose"
375 | if local_file:
376 | exportcmd += " file=ansible-export"
377 | changed = True
378 | response = sshcmd(module, device, cmd_timeout, exportcmd)
379 | if local_file:
380 | sftp = device.open_sftp()
381 | sftp.get("/ansible-export.rsc", exportfull)
382 | sftp.close()
383 | else:
384 | try:
385 | with open(exportfull, 'w') as exp:
386 | exp.write("# " + rosdev['username'] + "@" + identity +
387 | ", RouterOS " + version +": " + exportcmd + "\n")
388 | if timestamp:
389 | exp.write(response)
390 | else:
391 | no_ts = response.splitlines(1)[1:]
392 | exp.writelines(no_ts)
393 | exp.close()
394 | except Exception as export_error:
395 | if SHELLMODE:
396 | device.close()
397 | sys.exit("Export file error: " + str(export_error))
398 | safe_fail(module, device, msg=str(export_error),
399 | description='error writing to export file')
400 | if backup_dir:
401 | backup_dir = os.path.expanduser(backup_dir)
402 | backup_dir = os.path.realpath(backup_dir)
403 | if not os.path.exists(backup_dir):
404 | try:
405 | os.mkdir(backup_dir, 0775)
406 | except OSError as mkdir_error:
407 | if SHELLMODE:
408 | device.close()
409 | sys.exit("Backup directory error: " + str(mkdir_error))
410 | safe_fail(module, device, msg=str(mkdir_error),
411 | description='error creating backup directory')
412 | sftp = device.open_sftp()
413 | listdir = sftp.listdir()
414 | for item in listdir:
415 | if item.endswith('.backup'):
416 | bkp = os.path.join(backup_dir, item)
417 | if not os.path.exists(bkp):
418 | sftp.get(item, bkp)
419 | backup_files.append(item)
420 | sftp.close()
421 |
422 | if SHELLMODE:
423 | device.close()
424 | print "export_dir: %s" % export_dir
425 | print "export_file: %s" % export_file
426 | if backup_dir:
427 | print "backup_dir: %s" % backup_dir
428 | print "backup_files: %s" % ', '.join(backup_files)
429 | sys.exit(0)
430 |
431 | safe_exit(module, device, changed=changed,
432 | export_file=export_file, export_dir=export_dir,
433 | backup_files=backup_files, backup_dir=backup_dir,
434 | identity=identity, software_id=software_id)
435 |
436 | if __name__ == '__main__':
437 | if len(sys.argv) > 1 or SHELLMODE:
438 | print "Ansible MikroTik Library %s" % MIKROTIK_MODULE
439 | SHELLOPTS = parse_opts(sys.argv)
440 | SHELLMODE = True
441 | main()
442 |
--------------------------------------------------------------------------------
/library/mikrotik_facts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | """MikroTik RouterOS ansible facts gathering module"""
4 |
5 | import sys
6 | import re
7 | import socket
8 |
9 | HAS_SSHCLIENT = True
10 | SHELLMODE = False
11 | SHELLDEFS = {
12 | 'username': 'admin',
13 | 'password': '',
14 | 'key_filename': None,
15 | 'timeout': 30,
16 | 'port': 22,
17 | 'verbose': False
18 | }
19 | MIKROTIK_MODULE = '[github.com/nekitamo/ansible-mikrotik] v2017.07'
20 | DOCUMENTATION = """
21 | ---
22 |
23 | module: mikrotik_facts
24 | short_description: Gather facts from MikroTik RouterOS devices
25 | description:
26 | - Gather fact data (characteristics) of MikroTik RouterOS devices.
27 | - If you create router user 'ansible' with ssh-key you can omit username/password in playbooks
28 | return_data:
29 | - identity
30 | - license
31 | - resources
32 | - routerboard
33 | - health
34 | - users
35 | - packages
36 | - interfaces
37 | - ip addresses
38 | - mac addresses
39 | - misc info
40 | options:
41 | verbose:
42 | description:
43 | - Gather even more device facts (slower)
44 | required: no
45 | default: false
46 | port:
47 | description:
48 | - SSH listening port of the MikroTik device
49 | required: no
50 | default: 22
51 | hostname:
52 | description:
53 | - IP Address or hostname of the MikroTik device
54 | required: yes
55 | default: null
56 | username:
57 | description:
58 | - Username used to login to the device
59 | required: no
60 | default: ansible
61 | password:
62 | description:
63 | - Password used to login to the device
64 | required: no
65 | default: null
66 |
67 | """
68 | EXAMPLES = """
69 | - name: Gather MikroTik facts
70 | mikrotik_facts:
71 | hostname: "{{ inventory_hostname }}"
72 | username: admin
73 | """
74 | RETURN = """
75 | ansible_facts:
76 | description: Returns facts collected from the device
77 | returned: always
78 | type: dict
79 | """
80 | SHELL_USAGE = """
81 | mikrotik_facts.py --hostname= [--verbose] [--port=]
82 | [--username=] [--password=]
83 | """
84 |
85 | try:
86 | import paramiko
87 | except ImportError as import_error:
88 | HAS_SSHCLIENT = False
89 |
90 | try:
91 | from ansible.module_utils.basic import AnsibleModule
92 | except ImportError:
93 | SHELLMODE = True
94 | else:
95 | if sys.stdin.isatty():
96 | SHELLMODE = True
97 |
98 | def safe_fail(module, device=None, **kwargs):
99 | """closes device before module fail"""
100 | if device:
101 | device.close()
102 | module.fail_json(**kwargs)
103 |
104 | def safe_exit(module, device=None, **kwargs):
105 | """closes device before module exit"""
106 | if device:
107 | device.close()
108 | module.exit_json(**kwargs)
109 |
110 | def parse_opts(cmdline):
111 | """returns SHELLMODE command line options as dict"""
112 | options = SHELLDEFS
113 | for opt in cmdline:
114 | if opt.startswith('--'):
115 | try:
116 | arg, val = opt.split("=", 1)
117 | except ValueError:
118 | arg = opt
119 | val = True
120 | else:
121 | if val.lower() in ('no', 'false', '0'):
122 | val = False
123 | elif val.lower() in ('yes', 'true', '1'):
124 | val = True
125 | arg = arg[2:]
126 | if arg in options or arg == 'hostname':
127 | options[arg] = val
128 | else:
129 | print SHELL_USAGE
130 | sys.exit("Unknown option: --%s" % arg)
131 | if 'hostname' not in options:
132 | print SHELL_USAGE
133 | sys.exit("Hostname is required, specify with --hostname=")
134 | return options
135 |
136 | def device_connect(module, device, rosdev):
137 | """open ssh connection with or without ssh keys"""
138 | if SHELLMODE:
139 | sys.stdout.write("Opening SSH connection to %s(%s:%s)... "
140 | % (rosdev['hostname'], rosdev['ipaddress'], rosdev['port']))
141 | sys.stdout.flush()
142 | try:
143 | device.connect(rosdev['ipaddress'], username=rosdev['username'],
144 | key_filename=rosdev['key_filename'], port=rosdev['port'],
145 | timeout=rosdev['timeout'], password=rosdev['password'])
146 | except Exception:
147 | try:
148 | device.connect(rosdev['ipaddress'], username=rosdev['username'],
149 | password=rosdev['password'], port=rosdev['port'],
150 | timeout=rosdev['timeout'], allow_agent=False,
151 | look_for_keys=False)
152 | except Exception as ssh_error:
153 | if SHELLMODE:
154 | sys.exit("failed!\nSSH error: " + str(ssh_error))
155 | safe_fail(module, device, msg=str(ssh_error),
156 | description='error opening ssh connection to %s(%s:%s)' %
157 | (rosdev['hostname'], rosdev['ipaddress'], rosdev['port']))
158 | if SHELLMODE:
159 | print "succes."
160 |
161 | def sshcmd(module, device, timeout, command):
162 | """executes a command on the device, returns string"""
163 | try:
164 | _stdin, stdout, _stderr = device.exec_command(command, timeout=timeout)
165 | except Exception as ssh_error:
166 | if SHELLMODE:
167 | sys.exit("SSH command error: " + str(ssh_error))
168 | safe_fail(module, device, msg=str(ssh_error),
169 | description='SSH error while executing command')
170 | response = stdout.read()
171 | if not 'bad command name ' in response:
172 | if not 'syntax error ' in response:
173 | if not 'failure: ' in response:
174 | return response.rstrip()
175 | if SHELLMODE:
176 | print "Command: " + str(command)
177 | sys.exit("Error: " + str(response))
178 | safe_fail(module, device, msg=str(ssh_error),
179 | description='bad command name or syntax error')
180 |
181 | def parse_terse(device, key, command):
182 | """executes a command and returns list"""
183 | _stdin, stdout, _stderr = device.exec_command(command)
184 | vals = []
185 | for line in stdout.readlines():
186 | if key in line:
187 | val = line.split(key+'=')[1]
188 | vals.append(val.split(' ')[0])
189 | return vals
190 |
191 | def parse_facts(device, command, pfx=""):
192 | """executes a command and returns dict"""
193 | _stdin, stdout, _stderr = device.exec_command(command)
194 | facts = {}
195 | for line in stdout.readlines():
196 | if ':' in line:
197 | fact, value = line.partition(":")[::2]
198 | fact = fact.replace('-', '_')
199 | if pfx not in fact:
200 | facts[pfx + fact.strip()] = str(value.strip())
201 | else:
202 | facts[fact.strip()] = str(value.strip())
203 | return facts
204 |
205 | def vercmp(ver1, ver2):
206 | """quick and dirty version comparison from stackoverflow"""
207 | def normalize(ver):
208 | return [int(x) for x in re.sub(r'(\.0+)*$', '', ver).split(".")]
209 | return cmp(normalize(ver1), normalize(ver2))
210 |
211 | def main():
212 | rosdev = {}
213 | mtfacts = {}
214 | cmd_timeout = 30
215 | changed = False
216 | if not SHELLMODE:
217 | module = AnsibleModule(
218 | argument_spec=dict(
219 | verbose=dict(default=False, type='bool'),
220 | port=dict(default=22, type='int'),
221 | timeout=dict(default=30, type='float'),
222 | hostname=dict(required=True),
223 | key_filename=dict(default=None, type='path'),
224 | username=dict(default='ansible', type='str'),
225 | password=dict(default='', type='str', no_log=True),
226 | ), supports_check_mode=False
227 | )
228 | if not HAS_SSHCLIENT:
229 | safe_fail(module, msg='There was a problem loading module: ',
230 | error=str(import_error))
231 | verbose = module.params['verbose']
232 | rosdev['hostname'] = module.params['hostname']
233 | rosdev['username'] = module.params['username']
234 | rosdev['password'] = module.params['password']
235 | rosdev['key_filename'] = module.params['key_filename']
236 | rosdev['port'] = module.params['port']
237 | rosdev['timeout'] = module.params['timeout']
238 |
239 | else:
240 | if not HAS_SSHCLIENT:
241 | sys.exit("SSH client error: " + str(import_error))
242 | rosdev['hostname'] = SHELLOPTS['hostname']
243 | rosdev['username'] = SHELLOPTS['username']
244 | rosdev['password'] = SHELLOPTS['password']
245 | rosdev['key_filename'] = SHELLOPTS['key_filename']
246 | rosdev['port'] = SHELLOPTS['port']
247 | rosdev['timeout'] = SHELLOPTS['timeout']
248 | verbose = SHELLOPTS['verbose']
249 | module = None
250 |
251 | try:
252 | rosdev['ipaddress'] = socket.gethostbyname(rosdev['hostname'])
253 | except socket.gaierror as dns_error:
254 | if SHELLMODE:
255 | sys.exit("Hostname error: " + str(dns_error))
256 | safe_fail(module, msg=str(dns_error),
257 | description='error getting device address from hostname')
258 |
259 | device = paramiko.SSHClient()
260 | device.set_missing_host_key_policy(paramiko.AutoAddPolicy())
261 | device_connect(module, device, rosdev)
262 |
263 | mgmt = None
264 | mtfacts['management_ip_address'] = rosdev['ipaddress']
265 | identity = sshcmd(module, device, cmd_timeout, "system identity print")
266 | mtfacts['identity'] = str(identity.split(": ")[1])
267 | user_ssh_keys = parse_terse(device, "key-owner",
268 | "user ssh-keys print terse where user=" + rosdev['username'])
269 | if user_ssh_keys:
270 | mtfacts['user_ssh_keys'] = user_ssh_keys
271 | src = parse_terse(device, "address",
272 | 'user active print terse where name="' + rosdev['username'] + '" and via=ssh')
273 | if len(src) == 1:
274 | mtfacts['management_source_ip'] = src[0]
275 | con = parse_terse(device, "dst-address",
276 | 'ip firewall connection print terse where tcp-state=established and '
277 | + 'src-address~"' + src[0] + '" and dst-address~".*:' + str(rosdev['port'])
278 | + '"')
279 | if len(con) == 1:
280 | ifc = parse_terse(device, "interface",
281 | 'ip address print terse where address~"' + str(con[0]).split(":")[0] + '"')
282 | else:
283 | ifc = parse_terse(device, "interface",
284 | 'ip address print terse where address~"' + rosdev['ipaddress'] + '"')
285 | if len(ifc) == 1:
286 | mgmt = str(ifc[0])
287 |
288 | mtfacts.update(parse_facts(device, "system resource print without-paging"))
289 | mtfacts.update(parse_facts(device, "system routerboard print without-paging"))
290 | mtfacts.update(parse_facts(device, "system health print without-paging", "health_"))
291 | mtfacts.update(parse_facts(device, "system license print without-paging", "license_"))
292 | mtfacts.update(parse_facts(device, "ip cloud print without-paging", "cloud_"))
293 | if " " in mtfacts['version']:
294 | mtfacts['routeros_version'] = mtfacts['version'].split(" ")[0]
295 |
296 | mtfacts['enabled_packages'] = parse_terse(device, "name",
297 | "system package print terse without-paging where disabled=no")
298 | for pkg in mtfacts['enabled_packages']:
299 | if 'routeros' in pkg:
300 | mtfacts['enabled_packages'].remove(pkg)
301 | mtfacts['enabled_interfaces'] = parse_terse(device, "name",
302 | "interface print terse without-paging where disabled=no")
303 | if mgmt and mgmt in mtfacts['enabled_interfaces']:
304 | mtfacts['management_interface'] = mgmt
305 | mtfacts['ip_addresses'] = parse_terse(device, "address",
306 | "ip address print terse without-paging where disabled=no")
307 | mtfacts['mac_addresses'] = parse_terse(device, "mac-address",
308 | "interface print terse without-paging where disabled=no")
309 | mtfacts['remote_syslog'] = parse_terse(device, "remote",
310 | "system logging action print terse without-paging")
311 | email_server = parse_terse(device, "address", "tool e-mail export hide-sensitive")
312 | if email_server:
313 | mtfacts['email_server'] = email_server
314 | if 'wireless' in mtfacts['enabled_packages']:
315 | wifaces = parse_terse(device, "name",
316 | "interface wireless print terse without-paging")
317 | if wifaces:
318 | mtfacts['wireless_interfaces'] = wifaces
319 | if 'ipv6' in mtfacts['enabled_packages']:
320 | mtfacts['ipv6_addresses'] = parse_terse(device, "address",
321 | "ipv6 address print terse without-paging where disabled=no")
322 |
323 | if verbose:
324 | mtfacts.update(parse_facts(device, "ip ssh print without-paging", "ssh_"))
325 | mtfacts.update(parse_facts(device, "ip settings print without-paging", "ipv4_"))
326 | mtfacts.update(parse_facts(device, "system clock print without-paging", "clock_"))
327 | mtfacts.update(parse_facts(device, "snmp print without-paging", "snmp_"))
328 | mtfacts['disabled_packages'] = parse_terse(device, "name",
329 | "system package print terse without-paging where disabled=yes")
330 | mtfacts['scheduled_packages'] = parse_terse(device, "name",
331 | 'system package print terse without-paging where scheduled~"scheduled"')
332 | mtfacts['disabled_interfaces'] = parse_terse(device, "name",
333 | "interface print terse without-paging where disabled=yes")
334 | mtfacts.update(parse_facts(device,
335 | "interface bridge settings print without-paging", "bridge_"))
336 | mtfacts.update(parse_facts(device,
337 | "ip firewall connection tracking print without-paging", "conntrack_"))
338 | mtfacts['users'] = parse_terse(device, "name",
339 | "user print terse without-paging where disabled=no")
340 | mtfacts['mac_server_interfaces'] = parse_terse(device, "interface",
341 | "tool mac-server print terse without-paging where disabled=no")
342 | mtfacts['mac_winbox_interfaces'] = parse_terse(device, "interface",
343 | "tool mac-server mac-winbox print terse without-paging where disabled=no")
344 | mtfacts['ip_services'] = parse_terse(device, "name",
345 | "ip service print terse without-paging where disabled=no")
346 | mtfacts['neighbor_discovery_interfaces'] = parse_terse(device, "name",
347 | "ip neighbor discovery print terse without-paging where disabled=no")
348 | mtfacts['ethernet_interfaces'] = parse_terse(device, "name",
349 | "interface ethernet print terse without-paging")
350 | mtfacts['ethernet_switch_types'] = parse_terse(device, "type",
351 | "interface ethernet switch print terse without-paging")
352 | mtfacts['bridge_interfaces'] = parse_terse(device, "name",
353 | "interface bridge print terse without-paging")
354 | mtfacts.update(parse_facts(device,
355 | "system ntp client print without-paging", "ntp_client_"))
356 | if 'ntp' in mtfacts['enabled_packages']:
357 | mtfacts.update(parse_facts(device,
358 | "system ntp server print without-paging", "ntp_server_"))
359 | if 'ipv6' in mtfacts['enabled_packages']:
360 | mtfacts.update(parse_facts(device, "ipv6 settings print without-paging",
361 | "ipv6_"))
362 |
363 | if SHELLMODE:
364 | device.close()
365 | for fact in sorted(mtfacts):
366 | if isinstance(mtfacts[fact], list):
367 | print "%s: %s" % (fact, ', '.join(mtfacts[fact]))
368 | else:
369 | print "%s: %s" % (fact, mtfacts[fact])
370 | sys.exit(0)
371 |
372 | safe_exit(module, device, ansible_facts=mtfacts, changed=changed)
373 |
374 | if __name__ == '__main__':
375 | if len(sys.argv) > 1 or SHELLMODE:
376 | print "Ansible MikroTik Library %s" % MIKROTIK_MODULE
377 | SHELLOPTS = parse_opts(sys.argv)
378 | SHELLMODE = True
379 | main()
380 |
--------------------------------------------------------------------------------
/library/mikrotik_package.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | """MikroTik RouterOS package manager"""
4 |
5 | import sys
6 | import re
7 | import os
8 | import socket
9 | import time
10 |
11 | HAS_SSHCLIENT = True
12 | SHELLMODE = False
13 | SHELLDEFS = {
14 | 'username': 'admin',
15 | 'password': '',
16 | 'timeout': 60,
17 | 'port': 22,
18 | 'repository': 'routeros',
19 | 'packages': None,
20 | 'version': None,
21 | 'reboot': False
22 | # TODO:
23 | # 'reboot_timeout': 60,
24 | # 'reboot_wait': true,
25 | # 'default_packages': ['system', 'security', 'dhcp']
26 | }
27 | MIKROTIK_MODULE = '[github.com/nekitamo/ansible-mikrotik] v2017.03.23'
28 | DOCUMENTATION = """
29 | ---
30 | module: mikrotik_package
31 | short_description: MikroTik RouterOS package manager
32 | description:
33 | - MikroTik RouterOS package manager for desired state provisioning
34 | - Supports automatic install/enable/disable package operations with local package repository
35 | - If you create router user 'ansible' with ssh-key you can omit username/password in playbooks
36 | return_data:
37 | - routeros_version
38 | - enabled_packages
39 | - disabled_packages
40 | - scheduled_packages
41 | options:
42 | repository:
43 | description:
44 | - Preexisting directory with uncompressed RouterOS / package tree
45 | - Created either manually or with the included shell script (routeros/latest.sh)
46 | required: false
47 | default: 'routeros'
48 | packages:
49 | description:
50 | - List of desired RouterOS packages after provisioning
51 | - If omitted, currently enabled packages will be kept after upgrade or downgrade
52 | required: false
53 | default: null
54 | version:
55 | description:
56 | - desired RouterOS version, no change if omitted (use to add/remove packages)
57 | required: false
58 | default: null
59 | reboot:
60 | description:
61 | - Reboot device after package provisioning and wait until it gets online
62 | required: false
63 | default: false
64 | port:
65 | description:
66 | - SSH listening port of the MikroTik RouterOS device
67 | required: false
68 | default: 22
69 | hostname:
70 | description:
71 | - IP Address or hostname of the MikroTik RouterOS device
72 | required: true
73 | default: null
74 | username:
75 | description:
76 | - Username used to login to the MikroTik RouterOS device
77 | required: false
78 | default: ansible
79 | password:
80 | description:
81 | - Password used to login to the MikroTik RouterOS device
82 | required: false
83 | default: null
84 |
85 | """
86 | EXAMPLES = """
87 | # example playbook, requires RouterOS repo in ./routeros
88 |
89 | - name: Mikrotik RouterOS package management
90 | hosts: mikrotik_routers
91 | include_vars: routeros_auth.yml
92 | connection: local
93 |
94 | tasks:
95 |
96 | - name: upgrade routeros packages
97 | mikrotik_package:
98 | hostname: "{{ inventory_hostname }}"
99 | username: "{{ routeros_username }}"
100 | password: "{{ routeros_password }}"
101 | reboot: yes
102 | version: 6.38.5
103 |
104 | """
105 | RETURN = """
106 | routeros_version:
107 | description: actual RouterOS version after task execution
108 | returned: always
109 | type: string
110 | enabled_packages:
111 | description: actual list of enabled packages after task execution
112 | returned: always
113 | type: list
114 | disabled_packages:
115 | description: actual list of disabled packages after task execution
116 | returned: always
117 | type: list
118 | scheduled_packages:
119 | description: list of packages to be enabled or disabled after next reboot
120 | returned: always
121 | type: list
122 | """
123 | SHELL_USAGE = """
124 | mikrotik_package.py --hostname= --repository=
125 | [--packages=] [--reboot[=true|false|yes|no]]
126 | [--port=] [--username=] [--password=]
127 | """
128 |
129 | try:
130 | import paramiko
131 | except ImportError as import_error:
132 | HAS_SSHCLIENT = False
133 |
134 | try:
135 | from ansible.module_utils.basic import AnsibleModule
136 | except ImportError:
137 | SHELLMODE = True
138 | else:
139 | # ansible parameters on stdin?
140 | if sys.stdin.isatty():
141 | SHELLMODE = True
142 |
143 | def safe_fail(module, device=None, **kwargs):
144 | """closes device before module fail"""
145 | if device:
146 | device.close()
147 | module.fail_json(**kwargs)
148 |
149 | def safe_exit(module, device=None, **kwargs):
150 | """closes device before module exit"""
151 | if device:
152 | device.close()
153 | module.exit_json(**kwargs)
154 |
155 | def parse_opts(cmdline):
156 | """returns SHELLMODE command line options as dict"""
157 | options = SHELLDEFS
158 | for opt in cmdline:
159 | if opt.startswith('--'):
160 | try:
161 | arg, val = opt.split("=", 1)
162 | except ValueError:
163 | arg = opt
164 | val = True
165 | else:
166 | if val.lower() in ('no', 'false', '0'):
167 | val = False
168 | elif val.lower() in ('yes', 'true', '1'):
169 | val = True
170 | arg = arg[2:]
171 | if arg in options or arg == 'hostname':
172 | options[arg] = val
173 | else:
174 | print SHELL_USAGE
175 | sys.exit("Unknown option: --%s" % arg)
176 | if 'hostname' not in options:
177 | print SHELL_USAGE
178 | sys.exit("Hostname is required, specify with --hostname=")
179 | return options
180 |
181 | def device_connect(module, device, rosdev):
182 | """open ssh connection with or without ssh keys"""
183 | try:
184 | rosdev['hostname'] = socket.gethostbyname(rosdev['hostname'])
185 | except socket.gaierror as dns_error:
186 | if SHELLMODE:
187 | sys.exit("Hostname error: " + str(dns_error))
188 | safe_fail(module, device, msg=str(dns_error),
189 | description='error getting device address from hostname')
190 | if SHELLMODE:
191 | sys.stdout.write("Opening SSH connection to %s:%s... "
192 | % (rosdev['hostname'], rosdev['port']))
193 | sys.stdout.flush()
194 | try:
195 | device.connect(rosdev['hostname'], username=rosdev['username'],
196 | password=rosdev['password'], port=rosdev['port'],
197 | timeout=rosdev['timeout'])
198 | except Exception:
199 | try:
200 | device.connect(rosdev['hostname'], username=rosdev['username'],
201 | password=rosdev['password'], port=rosdev['port'],
202 | timeout=rosdev['timeout'], allow_agent=False,
203 | look_for_keys=False)
204 | except Exception as ssh_error:
205 | if SHELLMODE:
206 | sys.exit("failed!\nSSH error: " + str(ssh_error))
207 | safe_fail(module, device, msg=str(ssh_error),
208 | description='error opening ssh connection to %s' % rosdev['hostname'])
209 | if SHELLMODE:
210 | print "succes."
211 |
212 | def sshcmd(module, device, timeout, command):
213 | """executes a command on the device, returns string"""
214 | try:
215 | _stdin, stdout, _stderr = device.exec_command(command, timeout=timeout)
216 | except Exception as ssh_error:
217 | if SHELLMODE:
218 | sys.exit("SSH command error: " + str(ssh_error))
219 | safe_fail(module, device, msg=str(ssh_error),
220 | description='SSH error while executing command')
221 | response = stdout.read()
222 | if not 'bad command name ' in response:
223 | if not 'syntax error ' in response:
224 | if not 'failure: ' in response:
225 | return response.rstrip()
226 | if SHELLMODE:
227 | print "Command: " + str(command)
228 | sys.exit("Error: " + str(response))
229 | safe_fail(module, device, msg=str(ssh_error),
230 | description='bad command name or syntax error')
231 |
232 | def parse_terse(device, key, command):
233 | """executes a command and returns list"""
234 | _stdin, stdout, _stderr = device.exec_command(command)
235 | vals = []
236 | for line in stdout.readlines():
237 | if key in line:
238 | val = line.split(key+'=')[1]
239 | vals.append(val.split(' ')[0])
240 | return vals
241 |
242 | def parse_facts(device, command, pfx=""):
243 | """executes a command and returns dict"""
244 | _stdin, stdout, _stderr = device.exec_command(command)
245 | facts = {}
246 | for line in stdout.readlines():
247 | if ':' in line:
248 | fact, value = line.partition(":")[::2]
249 | fact = fact.replace('-', '_')
250 | if pfx not in fact:
251 | facts[pfx + fact.strip()] = str(value.strip())
252 | else:
253 | facts[fact.strip()] = str(value.strip())
254 | return facts
255 |
256 | def vercmp(ver1, ver2):
257 | """quick and dirty version comparison from stackoverflow"""
258 | def normalize(ver):
259 | return [int(x) for x in re.sub(r'(\.0+)*$', '', ver).split(".")]
260 | return cmp(normalize(ver1), normalize(ver2))
261 |
262 | def main():
263 | rosdev = {}
264 | upload = []
265 | enable = []
266 | disable = []
267 | cmd_timeout = 15
268 | reboot_timeout = 30
269 | default_packages = ['system', 'security']
270 | changed = False
271 | if not SHELLMODE:
272 | module = AnsibleModule(
273 | argument_spec=dict(
274 | repository=dict(default='routeros', type='path'),
275 | packages=dict(default=None, type='list'),
276 | version=dict(default=None, type='str'),
277 | reboot=dict(default=False, type='bool'),
278 | hostname=dict(required=True),
279 | username=dict(default='ansible', type='str'),
280 | password=dict(default='', type='str', no_log=True),
281 | port=dict(default=22, type='int'),
282 | timeout=dict(default=30, type='float')
283 | ), supports_check_mode=False
284 | )
285 | if not HAS_SSHCLIENT:
286 | safe_fail(module, msg='There was a problem loading module: ',
287 | error=str(import_error))
288 | repository = os.path.expanduser(module.params['repository'])
289 | packages = module.params['packages']
290 | version = module.params['version']
291 | reboot = module.params['reboot']
292 | rosdev['hostname'] = socket.gethostbyname(module.params['hostname'])
293 | rosdev['username'] = module.params['username']
294 | rosdev['password'] = module.params['password']
295 | rosdev['port'] = module.params['port']
296 | rosdev['timeout'] = module.params['timeout']
297 |
298 | else:
299 | if not HAS_SSHCLIENT:
300 | sys.exit("SSH client error: " + str(import_error))
301 | rosdev['hostname'] = SHELLOPTS['hostname']
302 | rosdev['username'] = SHELLOPTS['username']
303 | rosdev['password'] = SHELLOPTS['password']
304 | rosdev['port'] = SHELLOPTS['port']
305 | rosdev['timeout'] = SHELLOPTS['timeout']
306 | repository = os.path.expanduser(SHELLOPTS['repository'])
307 | packages = None
308 | if SHELLOPTS['packages']:
309 | packages = SHELLOPTS['packages'].split(",")
310 | version = SHELLOPTS['version']
311 | reboot = SHELLOPTS['reboot']
312 | module = None
313 |
314 | device = paramiko.SSHClient()
315 | device.set_missing_host_key_policy(paramiko.AutoAddPolicy())
316 |
317 | turn = 1
318 | while turn:
319 | if turn != 2:
320 | device_connect(module, device, rosdev)
321 |
322 | response = sshcmd(module, device, cmd_timeout,
323 | ":put [/system resource get version]")
324 | device_version = str(response.split(" ")[0])
325 | enabled_packages = parse_terse(device, "name",
326 | "system package print terse without-paging where disabled=no")
327 | disabled_packages = parse_terse(device, "name",
328 | "system package print terse without-paging where disabled=yes")
329 | scheduled_packages = parse_terse(device, "name",
330 | 'system package print terse without-paging where scheduled~"scheduled"')
331 | for pkg in enabled_packages:
332 | if 'routeros' in pkg:
333 | enabled_packages.remove(pkg)
334 | break
335 | if not packages:
336 | packages = list(enabled_packages)
337 | if turn > 1:
338 | break
339 | if not version:
340 | version = device_version
341 | diff = vercmp(device_version, version)
342 | if diff > 0:
343 | downgrade = True
344 | if SHELLMODE:
345 | print "Downgrading RouterOS: %s to %s" % (device_version, version)
346 | else:
347 | downgrade = False
348 | if vercmp(version, "6.37") >= 0:
349 | for pkg in packages:
350 | if 'wireless-' in pkg:
351 | packages.remove(pkg)
352 | packages.append('wireless')
353 | break
354 | response = sshcmd(module, device, cmd_timeout,
355 | ":put [/system resource get architecture-name]")
356 | arch = response.lower()
357 | if SHELLMODE and diff < 0:
358 | print "Upgrading RouterOS: %s to %s (%s)" % (device_version, version, arch)
359 | if arch == 'x86_64':
360 | arch = 'x86'
361 | for def_pkg in default_packages:
362 | if def_pkg not in packages:
363 | packages.append(def_pkg)
364 | for pkg in packages:
365 | if pkg in disabled_packages:
366 | enable.append(pkg)
367 | if device_version != version:
368 | upload.append(pkg)
369 | elif pkg not in enabled_packages:
370 | upload.append(pkg)
371 | elif device_version != version:
372 | upload.append(pkg)
373 | for pkg in enabled_packages:
374 | if pkg not in packages:
375 | disable.append(pkg)
376 | if SHELLMODE and upload:
377 | print "Uploading package(s): %s" % ', '.join(upload)
378 | for pkg in upload:
379 | if arch == 'x86':
380 | pkg = pkg + "-" + version + ".npk"
381 | else:
382 | pkg = pkg + "-" + version + "-" + arch + ".npk"
383 | ppath = os.path.join(repository, version, arch, pkg)
384 | if not os.path.exists(ppath):
385 | if SHELLMODE:
386 | device.close()
387 | sys.exit("package not found: " + str(pkg))
388 | safe_fail(module, device, msg=str(pkg),
389 | description='package not found')
390 | else:
391 | sftp = device.open_sftp()
392 | uploaded = sftp.listdir()
393 | if pkg in uploaded and SHELLMODE:
394 | print "- package %s found, overwritting..." % pkg
395 | try:
396 | sftp.put(ppath, pkg)
397 | except Exception as put_error:
398 | if SHELLMODE:
399 | sys.exit("Upload failed, SFTP error: " + str(put_error))
400 | safe_fail(module, device, msg=str(put_error),
401 | description='SFTP error, check disk space')
402 | sftp.close()
403 | changed = True
404 | if not upload:
405 | if scheduled_packages and (disable or enable):
406 | _res = sshcmd(module, device, cmd_timeout,
407 | 'system package unschedule [find scheduled~"scheduled"]')
408 | changed = True
409 | if SHELLMODE and disable:
410 | print "Disabling package(s): %s" % ', '.join(disable)
411 | for pkg in disable:
412 | _res = sshcmd(module, device, cmd_timeout,
413 | 'system package disable [find name~"'
414 | + pkg + '"]')
415 | changed = True
416 | if SHELLMODE and enable:
417 | print "Enabling package(s): %s" % ', '.join(enable)
418 | for pkg in enable:
419 | _res = sshcmd(module, device, cmd_timeout,
420 | 'system package enable [find name~"'
421 | + pkg + '"]')
422 | changed = True
423 | if not changed:
424 | break
425 | if reboot:
426 | if downgrade:
427 | cmd = "system package downgrade"
428 | else:
429 | cmd = "system reboot"
430 | _res = sshcmd(module, device, cmd_timeout, cmd)
431 | device.close()
432 | if SHELLMODE:
433 | print "Waiting %d seconds for reboot (/%s)..." % (reboot_timeout, cmd)
434 | time.sleep(reboot_timeout)
435 | turn += 1
436 | turn += 1
437 |
438 | if SHELLMODE:
439 | device.close()
440 | print "routeros_version: %s" % device_version
441 | print "enabled_packages: %s" % ', '.join(enabled_packages)
442 | if disabled_packages:
443 | print "disabled_packages: %s" % ', '.join(disabled_packages)
444 | if scheduled_packages:
445 | print "scheduled_packages: %s" % ', '.join(scheduled_packages)
446 | if not changed:
447 | print "Nothing changed."
448 | sys.exit(0)
449 |
450 | safe_exit(module, device, changed=changed,
451 | routeros_version=device_version,
452 | enabled_packages=enabled_packages,
453 | disabled_packages=disabled_packages,
454 | uploaded_packages=upload)
455 |
456 | if __name__ == '__main__':
457 | if len(sys.argv) > 1 or SHELLMODE:
458 | print "Ansible MikroTik Library %s" % MIKROTIK_MODULE
459 | SHELLOPTS = parse_opts(sys.argv)
460 | SHELLMODE = True
461 | main()
462 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | paramiko>=2.1
2 |
--------------------------------------------------------------------------------
/routeros/archive.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | ros_scheme="https:"
5 | ros_archive="$ros_scheme//www.mikrotik.com/download/archive"
6 | ros_repo=.
7 |
8 | if [ $# -eq 0 ]; then
9 | echo "Usage: archive.sh "
10 | fi
11 |
12 | cd $ros_repo > /dev/null 2>&1 || ros_repo=.
13 | wget -q -O- $ros_archive |
14 | grep -Po "(?<=a href=\")[^\"]*/routeros/[^\"]*" |
15 | while read pkg; do
16 | # version extraction
17 | version="$(echo $pkg | grep -Po '(?<=routeros/)[^/]*')"
18 | if [ "$version" == "$1" ]; then
19 | mkdir -p $version
20 | if echo $pkg | grep -q "/$version/all_packages-"; then
21 | arch="$(echo $pkg | grep -Po '(?<=all_packages-).*(?=-)')"
22 | new=$(wget -nv -cNP $version $ros_scheme$pkg 2>&1)
23 | if [ "${#new}" -gt "1" ]; then
24 | echo "- new package $(basename $pkg) downloaded into $ros_repo/$version"
25 | mkdir -p $version/$arch
26 | echo " extracting $(basename $pkg) into $ros_repo/$version/$arch"
27 | unzip -qu $version/$(basename $pkg) -d $version/$arch || rm $version/$(basename $pkg)
28 | else
29 | echo "- package $(basename $pkg) already in $ros_repo/$version"
30 | fi
31 | fi
32 | fi
33 | done
34 | exit 0
--------------------------------------------------------------------------------
/routeros/latest.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | # first run gets 1.5GB of files!
5 | ros_repo=routeros
6 | ros_latest="https://www.mikrotik.com/download"
7 |
8 | cd $ros_repo > /dev/null 2>&1 || ros_repo=.
9 | wget -q -O- $ros_latest |
10 | grep -Po "(?<=a href=\")[^\"]*/routeros/[^\"]*" |
11 | while read pkg; do
12 | ver="$(echo $pkg | grep -Po '(?<=routeros/)[^/]*')"
13 | if echo $ver | grep -qv rc; then
14 | mkdir -p $ver
15 | wget -nv -cNP $ver https:$pkg
16 | if echo $pkg | grep -q winbox; then
17 | wbv="$(echo $pkg | grep -Po '(?<=winbox/)[^/]*')"
18 | cp -u $ver/winbox.exe $ver/winbox-$wbv.exe
19 | fi
20 | fi
21 | done
22 | find . -name "all_packages*.zip" |
23 | while read f; do
24 | ver="$(echo $f | grep -Po '(?<=\./)[^/]*')"
25 | arch="$(echo $f | grep -Po '(?<=all_packages-).*(?=-)')"
26 | mkdir -p $ver/$arch
27 | unzip -q -u $f -d $ver/$arch || rm $f
28 | done
29 | find . -maxdepth 2 -name "dude*.npk" |
30 | while read n; do
31 | ver="$(echo $n | grep -Po '(?<=/).*(?=/)')"
32 | arch="$(echo $n | grep -Po '(?<=-).*(?=\.)' | grep -Eo '[a-z]{3,}' || echo 'x86' )"
33 | cp -u $n ./$ver/$arch/$(basename $n)
34 | done
35 | #find . -maxdepth 1 -type d -ctime +90 -regex ".*[0-9]" -exec rm -rf {} \;
36 | exit 0
37 |
--------------------------------------------------------------------------------
/routeros/update.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 | #
4 | # This script automatically dowloads bugfix+current RouterOS packages
5 | # from MikroTik web and creates an off-line repository for use with
6 | # mikrotik_package.py Ansible module. It may need some adjustments
7 | # from time to time as target MikroTik web changes (ros_archive).
8 | #
9 | script_version="v2017.07 by https://github.com/nekitamo"
10 | ros_scheme="https:"
11 | ros_archive="$ros_scheme//www.mikrotik.com/download/archive"
12 | ros_repo=routeros
13 | ros_cleanup=180
14 | ros_log=update.log
15 | ros_versions=versions.yml
16 | changed="False"
17 |
18 | cd $ros_repo > /dev/null 2>&1 || ros_repo=.
19 | echo "# MikroTik RouterOS repository script $script_version" > $ros_log
20 | echo "START: $(date --rfc-3339=seconds) in $(pwd)" >> $ros_log
21 | echo "---" > $ros_versions
22 | wget -q -O- $ros_archive |
23 | grep -Po "(?<=a href=\")[^\"]*/routeros/[^\"]*|(?<=>)[^<]*release tree[^<]*" |
24 | while read pkg; do
25 | # main processing loop for "*/routeros/*" urls
26 | if echo $pkg | grep -q "release tree"; then
27 | # release tree extraction
28 | release=$(echo ${pkg,,} | cut -d" " -f1)
29 | version="unknown"; retry=500 # skip pkgs before giving up search
30 | echo -n "$pkg: " >> $ros_log
31 | else
32 | if [ "$version" == "unknown" ]; then
33 | # version extraction
34 | version="$(echo $pkg | grep -Po '(?<=routeros/)[^/]*')"
35 | mkdir -p $version
36 | ln -snf $version $release
37 | echo "$version" >> $ros_log
38 | echo "routeros_$release: $version" >> $ros_versions
39 | # echo -n "routeros_$release=$version "
40 | fi
41 | if echo $pkg | grep -q "/$version/"; then
42 | # extract all_packages zips into / subfolders
43 | if echo $pkg | grep -q "/$version/all_packages-"; then
44 | arch="$(echo $pkg | grep -Po '(?<=all_packages-).*(?=-)')"
45 | new=$(wget -nv -cNP $version $ros_scheme$pkg 2>&1)
46 | if [ "${#new}" -gt "1" ]; then
47 | echo "- new package $(basename $pkg) downloaded into $ros_repo/$version" >> $ros_log
48 | mkdir -p $version/$arch
49 | echo " extracting $(basename $pkg) into $ros_repo/$version/$arch" >> $ros_log
50 | unzip -qu $version/$(basename $pkg) -d $version/$arch || rm $version/$(basename $pkg)
51 | if [ "$changed" == "False" ]; then changed="True"; fi
52 | else
53 | echo "- package $(basename $pkg) already in $ros_repo/$version" >> $ros_log
54 | fi
55 | fi
56 | if echo $pkg | grep -q "/$version/routeros-"; then
57 | # download routeros combo npks into / subfolders
58 | arch="$(echo $pkg | grep -Po '(?<=routeros-).*(?=-)')"
59 | mkdir -p $version/$arch
60 | new=$(wget -nv -cNP $version/$arch $ros_scheme$pkg 2>&1)
61 | if [ "${#new}" -gt "1" ]; then
62 | echo "- new package $(basename $pkg) downloaded into $ros_repo/$version/$arch" >> $ros_log
63 | if [ "$changed" == "False" ]; then changed="True"; fi
64 | else
65 | echo "- package $(basename $pkg) already in $ros_repo/$version/$arch" >> $ros_log
66 | fi
67 | fi
68 | if echo $pkg | grep -Eq ".*/$version/dude-.*\.npk"; then
69 | # download dude npks (MT site finally fixed for non-x86 architectures)
70 | arch="$(echo $pkg | grep -Po '(?<=-).*(?=\.)' | grep -Eo '[a-z]{3,}' || echo 'x86' )"
71 | mkdir -p $version/$arch
72 | new=$(wget -nv -cNP $version/$arch $ros_scheme$pkg 2>&1)
73 | if [ "${#new}" -gt "1" ]; then
74 | echo "- new package $(basename $pkg) downloaded into $ros_repo/$version/$arch" >> $ros_log
75 | if [ "$changed" == "False" ]; then changed="True"; fi
76 | else
77 | echo "- package $(basename $pkg) already in $ros_repo/$version/$arch" >> $ros_log
78 | fi
79 | fi
80 | else
81 | ((retry--))
82 | if [ "$retry" -eq "0" ]; then
83 | #echo "changed=$changed"
84 | break
85 | fi
86 | fi
87 | fi
88 | done
89 | if [ "$ros_cleanup" -gt "0" ]; then
90 | echo "CLEANUP: deleting subfolders older than $ros_cleanup day(s)..." >> $ros_log
91 | find . -maxdepth 1 -type d -ctime +$ros_cleanup -regex ".*[0-9]" -exec rm -rf {} \; >> $ros_log 2>&1
92 | fi
93 | echo "STOP: $(date --rfc-3339=seconds), repository size: $(du -hc | grep -v '\.' | cut -f1)" >> $ros_log
94 | exit 0
--------------------------------------------------------------------------------