├── .gitignore
├── LICENSE-CODE.md
├── LICENSE-DOC.md
├── README.md
├── cloudformation
├── .cfnlintrc.yaml
├── .yamllint.yaml
├── lights_off_aws.yaml
├── lights_off_aws_bonus_cloudformation_example.yaml
└── lights_off_aws_prereq.yaml
├── lights_off_aws.py
├── media
├── lights-off-aws-architecture-and-flow-thumb.png
└── lights-off-aws-architecture-and-flow.png
├── pylintrc
├── requirements.txt
└── setup.cfg
/.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 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE-CODE.md:
--------------------------------------------------------------------------------
1 | ### GNU GENERAL PUBLIC LICENSE
2 |
3 | Version 3, 29 June 2007
4 |
5 | Copyright (C) 2007 Free Software Foundation, Inc.
6 |
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | ### Preamble
12 |
13 | The GNU General Public License is a free, copyleft license for
14 | software and other kinds of works.
15 |
16 | The licenses for most software and other practical works are designed
17 | to take away your freedom to share and change the works. By contrast,
18 | the GNU General Public License is intended to guarantee your freedom
19 | to share and change all versions of a program--to make sure it remains
20 | free software for all its users. We, the Free Software Foundation, use
21 | the GNU General Public License for most of our software; it applies
22 | also to any other work released this way by its authors. You can apply
23 | it to your programs, too.
24 |
25 | When we speak of free software, we are referring to freedom, not
26 | price. Our General Public Licenses are designed to make sure that you
27 | have the freedom to distribute copies of free software (and charge for
28 | them if you wish), that you receive source code or can get it if you
29 | want it, that you can change the software or use pieces of it in new
30 | free programs, and that you know you can do these things.
31 |
32 | To protect your rights, we need to prevent others from denying you
33 | these rights or asking you to surrender the rights. Therefore, you
34 | have certain responsibilities if you distribute copies of the
35 | software, or if you modify it: responsibilities to respect the freedom
36 | of others.
37 |
38 | For example, if you distribute copies of such a program, whether
39 | gratis or for a fee, you must pass on to the recipients the same
40 | freedoms that you received. You must make sure that they, too, receive
41 | or can get the source code. And you must show them these terms so they
42 | know their rights.
43 |
44 | Developers that use the GNU GPL protect your rights with two steps:
45 | (1) assert copyright on the software, and (2) offer you this License
46 | giving you legal permission to copy, distribute and/or modify it.
47 |
48 | For the developers' and authors' protection, the GPL clearly explains
49 | that there is no warranty for this free software. For both users' and
50 | authors' sake, the GPL requires that modified versions be marked as
51 | changed, so that their problems will not be attributed erroneously to
52 | authors of previous versions.
53 |
54 | Some devices are designed to deny users access to install or run
55 | modified versions of the software inside them, although the
56 | manufacturer can do so. This is fundamentally incompatible with the
57 | aim of protecting users' freedom to change the software. The
58 | systematic pattern of such abuse occurs in the area of products for
59 | individuals to use, which is precisely where it is most unacceptable.
60 | Therefore, we have designed this version of the GPL to prohibit the
61 | practice for those products. If such problems arise substantially in
62 | other domains, we stand ready to extend this provision to those
63 | domains in future versions of the GPL, as needed to protect the
64 | freedom of users.
65 |
66 | Finally, every program is threatened constantly by software patents.
67 | States should not allow patents to restrict development and use of
68 | software on general-purpose computers, but in those that do, we wish
69 | to avoid the special danger that patents applied to a free program
70 | could make it effectively proprietary. To prevent this, the GPL
71 | assures that patents cannot be used to render the program non-free.
72 |
73 | The precise terms and conditions for copying, distribution and
74 | modification follow.
75 |
76 | ### TERMS AND CONDITIONS
77 |
78 | #### 0. Definitions.
79 |
80 | "This License" refers to version 3 of the GNU General Public License.
81 |
82 | "Copyright" also means copyright-like laws that apply to other kinds
83 | of works, such as semiconductor masks.
84 |
85 | "The Program" refers to any copyrightable work licensed under this
86 | License. Each licensee is addressed as "you". "Licensees" and
87 | "recipients" may be individuals or organizations.
88 |
89 | To "modify" a work means to copy from or adapt all or part of the work
90 | in a fashion requiring copyright permission, other than the making of
91 | an exact copy. The resulting work is called a "modified version" of
92 | the earlier work or a work "based on" the earlier work.
93 |
94 | A "covered work" means either the unmodified Program or a work based
95 | on the Program.
96 |
97 | To "propagate" a work means to do anything with it that, without
98 | permission, would make you directly or secondarily liable for
99 | infringement under applicable copyright law, except executing it on a
100 | computer or modifying a private copy. Propagation includes copying,
101 | distribution (with or without modification), making available to the
102 | public, and in some countries other activities as well.
103 |
104 | To "convey" a work means any kind of propagation that enables other
105 | parties to make or receive copies. Mere interaction with a user
106 | through a computer network, with no transfer of a copy, is not
107 | conveying.
108 |
109 | An interactive user interface displays "Appropriate Legal Notices" to
110 | the extent that it includes a convenient and prominently visible
111 | feature that (1) displays an appropriate copyright notice, and (2)
112 | tells the user that there is no warranty for the work (except to the
113 | extent that warranties are provided), that licensees may convey the
114 | work under this License, and how to view a copy of this License. If
115 | the interface presents a list of user commands or options, such as a
116 | menu, a prominent item in the list meets this criterion.
117 |
118 | #### 1. Source Code.
119 |
120 | The "source code" for a work means the preferred form of the work for
121 | making modifications to it. "Object code" means any non-source form of
122 | a work.
123 |
124 | A "Standard Interface" means an interface that either is an official
125 | standard defined by a recognized standards body, or, in the case of
126 | interfaces specified for a particular programming language, one that
127 | is widely used among developers working in that language.
128 |
129 | The "System Libraries" of an executable work include anything, other
130 | than the work as a whole, that (a) is included in the normal form of
131 | packaging a Major Component, but which is not part of that Major
132 | Component, and (b) serves only to enable use of the work with that
133 | Major Component, or to implement a Standard Interface for which an
134 | implementation is available to the public in source code form. A
135 | "Major Component", in this context, means a major essential component
136 | (kernel, window system, and so on) of the specific operating system
137 | (if any) on which the executable work runs, or a compiler used to
138 | produce the work, or an object code interpreter used to run it.
139 |
140 | The "Corresponding Source" for a work in object code form means all
141 | the source code needed to generate, install, and (for an executable
142 | work) run the object code and to modify the work, including scripts to
143 | control those activities. However, it does not include the work's
144 | System Libraries, or general-purpose tools or generally available free
145 | programs which are used unmodified in performing those activities but
146 | which are not part of the work. For example, Corresponding Source
147 | includes interface definition files associated with source files for
148 | the work, and the source code for shared libraries and dynamically
149 | linked subprograms that the work is specifically designed to require,
150 | such as by intimate data communication or control flow between those
151 | subprograms and other parts of the work.
152 |
153 | The Corresponding Source need not include anything that users can
154 | regenerate automatically from other parts of the Corresponding Source.
155 |
156 | The Corresponding Source for a work in source code form is that same
157 | work.
158 |
159 | #### 2. Basic Permissions.
160 |
161 | All rights granted under this License are granted for the term of
162 | copyright on the Program, and are irrevocable provided the stated
163 | conditions are met. This License explicitly affirms your unlimited
164 | permission to run the unmodified Program. The output from running a
165 | covered work is covered by this License only if the output, given its
166 | content, constitutes a covered work. This License acknowledges your
167 | rights of fair use or other equivalent, as provided by copyright law.
168 |
169 | You may make, run and propagate covered works that you do not convey,
170 | without conditions so long as your license otherwise remains in force.
171 | You may convey covered works to others for the sole purpose of having
172 | them make modifications exclusively for you, or provide you with
173 | facilities for running those works, provided that you comply with the
174 | terms of this License in conveying all material for which you do not
175 | control copyright. Those thus making or running the covered works for
176 | you must do so exclusively on your behalf, under your direction and
177 | control, on terms that prohibit them from making any copies of your
178 | copyrighted material outside their relationship with you.
179 |
180 | Conveying under any other circumstances is permitted solely under the
181 | conditions stated below. Sublicensing is not allowed; section 10 makes
182 | it unnecessary.
183 |
184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
185 |
186 | No covered work shall be deemed part of an effective technological
187 | measure under any applicable law fulfilling obligations under article
188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
189 | similar laws prohibiting or restricting circumvention of such
190 | measures.
191 |
192 | When you convey a covered work, you waive any legal power to forbid
193 | circumvention of technological measures to the extent such
194 | circumvention is effected by exercising rights under this License with
195 | respect to the covered work, and you disclaim any intention to limit
196 | operation or modification of the work as a means of enforcing, against
197 | the work's users, your or third parties' legal rights to forbid
198 | circumvention of technological measures.
199 |
200 | #### 4. Conveying Verbatim Copies.
201 |
202 | You may convey verbatim copies of the Program's source code as you
203 | receive it, in any medium, provided that you conspicuously and
204 | appropriately publish on each copy an appropriate copyright notice;
205 | keep intact all notices stating that this License and any
206 | non-permissive terms added in accord with section 7 apply to the code;
207 | keep intact all notices of the absence of any warranty; and give all
208 | recipients a copy of this License along with the Program.
209 |
210 | You may charge any price or no price for each copy that you convey,
211 | and you may offer support or warranty protection for a fee.
212 |
213 | #### 5. Conveying Modified Source Versions.
214 |
215 | You may convey a work based on the Program, or the modifications to
216 | produce it from the Program, in the form of source code under the
217 | terms of section 4, provided that you also meet all of these
218 | conditions:
219 |
220 | - a) The work must carry prominent notices stating that you modified
221 | it, and giving a relevant date.
222 | - b) The work must carry prominent notices stating that it is
223 | released under this License and any conditions added under
224 | section 7. This requirement modifies the requirement in section 4
225 | to "keep intact all notices".
226 | - c) You must license the entire work, as a whole, under this
227 | License to anyone who comes into possession of a copy. This
228 | License will therefore apply, along with any applicable section 7
229 | additional terms, to the whole of the work, and all its parts,
230 | regardless of how they are packaged. This License gives no
231 | permission to license the work in any other way, but it does not
232 | invalidate such permission if you have separately received it.
233 | - d) If the work has interactive user interfaces, each must display
234 | Appropriate Legal Notices; however, if the Program has interactive
235 | interfaces that do not display Appropriate Legal Notices, your
236 | work need not make them do so.
237 |
238 | A compilation of a covered work with other separate and independent
239 | works, which are not by their nature extensions of the covered work,
240 | and which are not combined with it such as to form a larger program,
241 | in or on a volume of a storage or distribution medium, is called an
242 | "aggregate" if the compilation and its resulting copyright are not
243 | used to limit the access or legal rights of the compilation's users
244 | beyond what the individual works permit. Inclusion of a covered work
245 | in an aggregate does not cause this License to apply to the other
246 | parts of the aggregate.
247 |
248 | #### 6. Conveying Non-Source Forms.
249 |
250 | You may convey a covered work in object code form under the terms of
251 | sections 4 and 5, provided that you also convey the machine-readable
252 | Corresponding Source under the terms of this License, in one of these
253 | ways:
254 |
255 | - a) Convey the object code in, or embodied in, a physical product
256 | (including a physical distribution medium), accompanied by the
257 | Corresponding Source fixed on a durable physical medium
258 | customarily used for software interchange.
259 | - b) Convey the object code in, or embodied in, a physical product
260 | (including a physical distribution medium), accompanied by a
261 | written offer, valid for at least three years and valid for as
262 | long as you offer spare parts or customer support for that product
263 | model, to give anyone who possesses the object code either (1) a
264 | copy of the Corresponding Source for all the software in the
265 | product that is covered by this License, on a durable physical
266 | medium customarily used for software interchange, for a price no
267 | more than your reasonable cost of physically performing this
268 | conveying of source, or (2) access to copy the Corresponding
269 | Source from a network server at no charge.
270 | - c) Convey individual copies of the object code with a copy of the
271 | written offer to provide the Corresponding Source. This
272 | alternative is allowed only occasionally and noncommercially, and
273 | only if you received the object code with such an offer, in accord
274 | with subsection 6b.
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 | - e) Convey the object code using peer-to-peer transmission,
288 | provided you inform other peers where the object code and
289 | Corresponding Source of the work are being offered to the general
290 | public at no charge under subsection 6d.
291 |
292 | A separable portion of the object code, whose source code is excluded
293 | from the Corresponding Source as a System Library, need not be
294 | included in conveying the object code work.
295 |
296 | A "User Product" is either (1) a "consumer product", which means any
297 | tangible personal property which is normally used for personal,
298 | family, or household purposes, or (2) anything designed or sold for
299 | incorporation into a dwelling. In determining whether a product is a
300 | consumer product, doubtful cases shall be resolved in favor of
301 | coverage. For a particular product received by a particular user,
302 | "normally used" refers to a typical or common use of that class of
303 | product, regardless of the status of the particular user or of the way
304 | in which the particular user actually uses, or expects or is expected
305 | to use, the product. A product is a consumer product regardless of
306 | whether the product has substantial commercial, industrial or
307 | non-consumer uses, unless such uses represent the only significant
308 | 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
312 | install and execute modified versions of a covered work in that User
313 | Product from a modified version of its Corresponding Source. The
314 | information must suffice to ensure that the continued functioning of
315 | the modified object code is in no case prevented or interfered with
316 | solely because 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
331 | updates for a work that has been modified or installed by the
332 | recipient, or for the User Product in which it has been modified or
333 | installed. Access to a network may be denied when the modification
334 | itself materially and adversely affects the operation of the network
335 | or violates the rules and protocols for communication across the
336 | network.
337 |
338 | Corresponding Source conveyed, and Installation Information provided,
339 | in accord with this section must be in a format that is publicly
340 | documented (and with an implementation available to the public in
341 | source code form), and must require no special password or key for
342 | unpacking, reading or copying.
343 |
344 | #### 7. Additional Terms.
345 |
346 | "Additional permissions" are terms that supplement the terms of this
347 | License by making exceptions from one or more of its conditions.
348 | Additional permissions that are applicable to the entire Program shall
349 | be treated as though they were included in this License, to the extent
350 | that they are valid under applicable law. If additional permissions
351 | apply only to part of the Program, that part may be used separately
352 | under those permissions, but the entire Program remains governed by
353 | this License without regard to the additional permissions.
354 |
355 | When you convey a copy of a covered work, you may at your option
356 | remove any additional permissions from that copy, or from any part of
357 | it. (Additional permissions may be written to require their own
358 | removal in certain cases when you modify the work.) You may place
359 | additional permissions on material, added by you to a covered work,
360 | for which you have or can give appropriate copyright permission.
361 |
362 | Notwithstanding any other provision of this License, for material you
363 | add to a covered work, you may (if authorized by the copyright holders
364 | of that material) supplement the terms of this License with terms:
365 |
366 | - a) Disclaiming warranty or limiting liability differently from the
367 | terms of sections 15 and 16 of this License; or
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 | - c) Prohibiting misrepresentation of the origin of that material,
372 | or requiring that modified versions of such material be marked in
373 | reasonable ways as different from the original version; or
374 | - d) Limiting the use for publicity purposes of names of licensors
375 | or authors of the material; or
376 | - e) Declining to grant rights under trademark law for use of some
377 | trade names, trademarks, or service marks; or
378 | - f) Requiring indemnification of licensors and authors of that
379 | material by anyone who conveys the material (or modified versions
380 | of it) with contractual assumptions of liability to the recipient,
381 | for any liability that these contractual assumptions directly
382 | impose on those licensors and authors.
383 |
384 | All other non-permissive additional terms are considered "further
385 | restrictions" within the meaning of section 10. If the Program as you
386 | received it, or any part of it, contains a notice stating that it is
387 | governed by this License along with a term that is a further
388 | restriction, you may remove that term. If a license document contains
389 | a further restriction but permits relicensing or conveying under this
390 | License, you may add to a covered work material governed by the terms
391 | of that license document, provided that the further restriction does
392 | not survive such relicensing or conveying.
393 |
394 | If you add terms to a covered work in accord with this section, you
395 | must place, in the relevant source files, a statement of the
396 | additional terms that apply to those files, or a notice indicating
397 | where to find the applicable terms.
398 |
399 | Additional terms, permissive or non-permissive, may be stated in the
400 | form of a separately written license, or stated as exceptions; the
401 | above requirements apply either way.
402 |
403 | #### 8. Termination.
404 |
405 | You may not propagate or modify a covered work except as expressly
406 | provided under this License. Any attempt otherwise to propagate or
407 | modify it is void, and will automatically terminate your rights under
408 | this License (including any patent licenses granted under the third
409 | paragraph of section 11).
410 |
411 | However, if you cease all violation of this License, then your license
412 | from a particular copyright holder is reinstated (a) provisionally,
413 | unless and until the copyright holder explicitly and finally
414 | terminates your license, and (b) permanently, if the copyright holder
415 | fails to notify you of the violation by some reasonable means prior to
416 | 60 days after the cessation.
417 |
418 | Moreover, your license from a particular copyright holder is
419 | reinstated permanently if the copyright holder notifies you of the
420 | violation by some reasonable means, this is the first time you have
421 | received notice of violation of this License (for any work) from that
422 | copyright holder, and you cure the violation prior to 30 days after
423 | your receipt of the notice.
424 |
425 | Termination of your rights under this section does not terminate the
426 | licenses of parties who have received copies or rights from you under
427 | this License. If your rights have been terminated and not permanently
428 | reinstated, you do not qualify to receive new licenses for the same
429 | material under section 10.
430 |
431 | #### 9. Acceptance Not Required for Having Copies.
432 |
433 | You are not required to accept this License in order to receive or run
434 | a copy of the Program. Ancillary propagation of a covered work
435 | occurring solely as a consequence of using peer-to-peer transmission
436 | to receive a copy likewise does not require acceptance. However,
437 | nothing other than this License grants you permission to propagate or
438 | modify any covered work. These actions infringe copyright if you do
439 | not accept this License. Therefore, by modifying or propagating a
440 | covered work, you indicate your acceptance of this License to do so.
441 |
442 | #### 10. Automatic Licensing of Downstream Recipients.
443 |
444 | Each time you convey a covered work, the recipient automatically
445 | receives a license from the original licensors, to run, modify and
446 | propagate that work, subject to this License. You are not responsible
447 | for enforcing compliance by third parties with this License.
448 |
449 | An "entity transaction" is a transaction transferring control of an
450 | organization, or substantially all assets of one, or subdividing an
451 | organization, or merging organizations. If propagation of a covered
452 | work results from an entity transaction, each party to that
453 | transaction who receives a copy of the work also receives whatever
454 | licenses to the work the party's predecessor in interest had or could
455 | give under the previous paragraph, plus a right to possession of the
456 | Corresponding Source of the work from the predecessor in interest, if
457 | the predecessor has it or can get it with reasonable efforts.
458 |
459 | You may not impose any further restrictions on the exercise of the
460 | rights granted or affirmed under this License. For example, you may
461 | not impose a license fee, royalty, or other charge for exercise of
462 | rights granted under this License, and you may not initiate litigation
463 | (including a cross-claim or counterclaim in a lawsuit) alleging that
464 | any patent claim is infringed by making, using, selling, offering for
465 | sale, or importing the Program or any portion of it.
466 |
467 | #### 11. Patents.
468 |
469 | A "contributor" is a copyright holder who authorizes use under this
470 | License of the Program or a work on which the Program is based. The
471 | work thus licensed is called the contributor's "contributor version".
472 |
473 | A contributor's "essential patent claims" are all patent claims owned
474 | or controlled by the contributor, whether already acquired or
475 | hereafter acquired, that would be infringed by some manner, permitted
476 | by this License, of making, using, or selling its contributor version,
477 | but do not include claims that would be infringed only as a
478 | consequence of further modification of the contributor version. For
479 | purposes of this definition, "control" includes the right to grant
480 | patent sublicenses in a manner consistent with the requirements of
481 | this License.
482 |
483 | Each contributor grants you a non-exclusive, worldwide, royalty-free
484 | patent license under the contributor's essential patent claims, to
485 | make, use, sell, offer for sale, import and otherwise run, modify and
486 | propagate the contents of its contributor version.
487 |
488 | In the following three paragraphs, a "patent license" is any express
489 | agreement or commitment, however denominated, not to enforce a patent
490 | (such as an express permission to practice a patent or covenant not to
491 | sue for patent infringement). To "grant" such a patent license to a
492 | party means to make such an agreement or commitment not to enforce a
493 | patent against the party.
494 |
495 | If you convey a covered work, knowingly relying on a patent license,
496 | and the Corresponding Source of the work is not available for anyone
497 | to copy, free of charge and under the terms of this License, through a
498 | publicly available network server or other readily accessible means,
499 | then you must either (1) cause the Corresponding Source to be so
500 | available, or (2) arrange to deprive yourself of the benefit of the
501 | patent license for this particular work, or (3) arrange, in a manner
502 | consistent with the requirements of this License, to extend the patent
503 | license to downstream recipients. "Knowingly relying" means you have
504 | actual knowledge that, but for the patent license, your conveying the
505 | covered work in a country, or your recipient's use of the covered work
506 | in a country, would infringe one or more identifiable patents in that
507 | country that you have reason to believe are valid.
508 |
509 | If, pursuant to or in connection with a single transaction or
510 | arrangement, you convey, or propagate by procuring conveyance of, a
511 | covered work, and grant a patent license to some of the parties
512 | receiving the covered work authorizing them to use, propagate, modify
513 | or convey a specific copy of the covered work, then the patent license
514 | you grant is automatically extended to all recipients of the covered
515 | work and works based on it.
516 |
517 | A patent license is "discriminatory" if it does not include within the
518 | scope of its coverage, prohibits the exercise of, or is conditioned on
519 | the non-exercise of one or more of the rights that are specifically
520 | granted under this License. You may not convey a covered work if you
521 | are a party to an arrangement with a third party that is in the
522 | business of distributing software, under which you make payment to the
523 | third party based on the extent of your activity of conveying the
524 | work, and under which the third party grants, to any of the parties
525 | who would receive the covered work from you, a discriminatory patent
526 | license (a) in connection with copies of the covered work conveyed by
527 | you (or copies made from those copies), or (b) primarily for and in
528 | connection with specific products or compilations that contain the
529 | covered work, unless you entered into that arrangement, or that patent
530 | license was granted, prior to 28 March 2007.
531 |
532 | Nothing in this License shall be construed as excluding or limiting
533 | any implied license or other defenses to infringement that may
534 | otherwise be available to you under applicable patent law.
535 |
536 | #### 12. No Surrender of Others' Freedom.
537 |
538 | If conditions are imposed on you (whether by court order, agreement or
539 | otherwise) that contradict the conditions of this License, they do not
540 | excuse you from the conditions of this License. If you cannot convey a
541 | covered work so as to satisfy simultaneously your obligations under
542 | this License and any other pertinent obligations, then as a
543 | consequence you may not convey it at all. For example, if you agree to
544 | terms that obligate you to collect a royalty for further conveying
545 | from those to whom you convey the Program, the only way you could
546 | satisfy both those terms and this License would be to refrain entirely
547 | from conveying the Program.
548 |
549 | #### 13. Use with the GNU Affero General Public License.
550 |
551 | Notwithstanding any other provision of this License, you have
552 | permission to link or combine any covered work with a work licensed
553 | under version 3 of the GNU Affero General Public License into a single
554 | combined work, and to convey the resulting work. The terms of this
555 | License will continue to apply to the part which is the covered work,
556 | but the special requirements of the GNU Affero General Public License,
557 | section 13, concerning interaction through a network will apply to the
558 | combination as such.
559 |
560 | #### 14. Revised Versions of this License.
561 |
562 | The Free Software Foundation may publish revised and/or new versions
563 | of the GNU General Public License from time to time. Such new versions
564 | will be similar in spirit to the present version, but may differ in
565 | detail to address new problems or concerns.
566 |
567 | Each version is given a distinguishing version number. If the Program
568 | specifies that a certain numbered version of the GNU General Public
569 | License "or any later version" applies to it, you have the option of
570 | following the terms and conditions either of that numbered version or
571 | of any later version published by the Free Software Foundation. If the
572 | Program does not specify a version number of the GNU General Public
573 | License, you may choose any version ever published by the Free
574 | Software Foundation.
575 |
576 | If the Program specifies that a proxy can decide which future versions
577 | of the GNU General Public License can be used, that proxy's public
578 | statement of acceptance of a version permanently authorizes you to
579 | choose that version for the Program.
580 |
581 | Later license versions may give you additional or different
582 | permissions. However, no additional obligations are imposed on any
583 | author or copyright holder as a result of your choosing to follow a
584 | later version.
585 |
586 | #### 15. Disclaimer of Warranty.
587 |
588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
596 | CORRECTION.
597 |
598 | #### 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
609 |
610 | #### 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | ### How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these
626 | terms.
627 |
628 | To do so, attach the following notices to the program. It is safest to
629 | attach them to the start of each source file to most effectively state
630 | the exclusion of warranty; and each file should have at least the
631 | "copyright" line and a pointer to where the full notice is found.
632 |
633 |
634 | Copyright (C)
635 |
636 | This program is free software: you can redistribute it and/or modify
637 | it under the terms of the GNU General Public License as published by
638 | the Free Software Foundation, either version 3 of the License, or
639 | (at your option) any later version.
640 |
641 | This program is distributed in the hope that it will be useful,
642 | but WITHOUT ANY WARRANTY; without even the implied warranty of
643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
644 | GNU General Public License for more details.
645 |
646 | You should have received a copy of the GNU General Public License
647 | along with this program. If not, see .
648 |
649 | Also add information on how to contact you by electronic and paper
650 | 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 | Copyright (C)
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
661 | appropriate parts of the General Public License. Of course, your
662 | program's commands might be different; for a GUI interface, you would
663 | use an "about box".
664 |
665 | You should also get your employer (if you work as a programmer) or
666 | school, if any, to sign a "copyright disclaimer" for the program, if
667 | necessary. For more information on this, and how to apply and follow
668 | the GNU GPL, see .
669 |
670 | The GNU General Public License does not permit incorporating your
671 | program into proprietary programs. If your program is a subroutine
672 | library, you may consider it more useful to permit linking proprietary
673 | applications with the library. If this is what you want to do, use the
674 | GNU Lesser General Public License instead of this License. But first,
675 | please read .
676 |
--------------------------------------------------------------------------------
/LICENSE-DOC.md:
--------------------------------------------------------------------------------
1 | ### GNU Free Documentation License
2 |
3 | Version 1.3, 3 November 2008
4 |
5 | Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation,
6 | Inc.
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | #### 0. PREAMBLE
12 |
13 | The purpose of this License is to make a manual, textbook, or other
14 | functional and useful document "free" in the sense of freedom: to
15 | assure everyone the effective freedom to copy and redistribute it,
16 | with or without modifying it, either commercially or noncommercially.
17 | Secondarily, this License preserves for the author and publisher a way
18 | to get credit for their work, while not being considered responsible
19 | for modifications made by others.
20 |
21 | This License is a kind of "copyleft", which means that derivative
22 | works of the document must themselves be free in the same sense. It
23 | complements the GNU General Public License, which is a copyleft
24 | license designed for free software.
25 |
26 | We have designed this License in order to use it for manuals for free
27 | software, because free software needs free documentation: a free
28 | program should come with manuals providing the same freedoms that the
29 | software does. But this License is not limited to software manuals; it
30 | can be used for any textual work, regardless of subject matter or
31 | whether it is published as a printed book. We recommend this License
32 | principally for works whose purpose is instruction or reference.
33 |
34 | #### 1. APPLICABILITY AND DEFINITIONS
35 |
36 | This License applies to any manual or other work, in any medium, that
37 | contains a notice placed by the copyright holder saying it can be
38 | distributed under the terms of this License. Such a notice grants a
39 | world-wide, royalty-free license, unlimited in duration, to use that
40 | work under the conditions stated herein. The "Document", below, refers
41 | to any such manual or work. Any member of the public is a licensee,
42 | and is addressed as "you". You accept the license if you copy, modify
43 | or distribute the work in a way requiring permission under copyright
44 | law.
45 |
46 | A "Modified Version" of the Document means any work containing the
47 | Document or a portion of it, either copied verbatim, or with
48 | modifications and/or translated into another language.
49 |
50 | A "Secondary Section" is a named appendix or a front-matter section of
51 | the Document that deals exclusively with the relationship of the
52 | publishers or authors of the Document to the Document's overall
53 | subject (or to related matters) and contains nothing that could fall
54 | directly within that overall subject. (Thus, if the Document is in
55 | part a textbook of mathematics, a Secondary Section may not explain
56 | any mathematics.) The relationship could be a matter of historical
57 | connection with the subject or with related matters, or of legal,
58 | commercial, philosophical, ethical or political position regarding
59 | them.
60 |
61 | The "Invariant Sections" are certain Secondary Sections whose titles
62 | are designated, as being those of Invariant Sections, in the notice
63 | that says that the Document is released under this License. If a
64 | section does not fit the above definition of Secondary then it is not
65 | allowed to be designated as Invariant. The Document may contain zero
66 | Invariant Sections. If the Document does not identify any Invariant
67 | Sections then there are none.
68 |
69 | The "Cover Texts" are certain short passages of text that are listed,
70 | as Front-Cover Texts or Back-Cover Texts, in the notice that says that
71 | the Document is released under this License. A Front-Cover Text may be
72 | at most 5 words, and a Back-Cover Text may be at most 25 words.
73 |
74 | A "Transparent" copy of the Document means a machine-readable copy,
75 | represented in a format whose specification is available to the
76 | general public, that is suitable for revising the document
77 | straightforwardly with generic text editors or (for images composed of
78 | pixels) generic paint programs or (for drawings) some widely available
79 | drawing editor, and that is suitable for input to text formatters or
80 | for automatic translation to a variety of formats suitable for input
81 | to text formatters. A copy made in an otherwise Transparent file
82 | format whose markup, or absence of markup, has been arranged to thwart
83 | or discourage subsequent modification by readers is not Transparent.
84 | An image format is not Transparent if used for any substantial amount
85 | of text. A copy that is not "Transparent" is called "Opaque".
86 |
87 | Examples of suitable formats for Transparent copies include plain
88 | ASCII without markup, Texinfo input format, LaTeX input format, SGML
89 | or XML using a publicly available DTD, and standard-conforming simple
90 | HTML, PostScript or PDF designed for human modification. Examples of
91 | transparent image formats include PNG, XCF and JPG. Opaque formats
92 | include proprietary formats that can be read and edited only by
93 | proprietary word processors, SGML or XML for which the DTD and/or
94 | processing tools are not generally available, and the
95 | machine-generated HTML, PostScript or PDF produced by some word
96 | processors for output purposes only.
97 |
98 | The "Title Page" means, for a printed book, the title page itself,
99 | plus such following pages as are needed to hold, legibly, the material
100 | this License requires to appear in the title page. For works in
101 | formats which do not have any title page as such, "Title Page" means
102 | the text near the most prominent appearance of the work's title,
103 | preceding the beginning of the body of the text.
104 |
105 | The "publisher" means any person or entity that distributes copies of
106 | the Document to the public.
107 |
108 | A section "Entitled XYZ" means a named subunit of the Document whose
109 | title either is precisely XYZ or contains XYZ in parentheses following
110 | text that translates XYZ in another language. (Here XYZ stands for a
111 | specific section name mentioned below, such as "Acknowledgements",
112 | "Dedications", "Endorsements", or "History".) To "Preserve the Title"
113 | of such a section when you modify the Document means that it remains a
114 | section "Entitled XYZ" according to this definition.
115 |
116 | The Document may include Warranty Disclaimers next to the notice which
117 | states that this License applies to the Document. These Warranty
118 | Disclaimers are considered to be included by reference in this
119 | License, but only as regards disclaiming warranties: any other
120 | implication that these Warranty Disclaimers may have is void and has
121 | no effect on the meaning of this License.
122 |
123 | #### 2. VERBATIM COPYING
124 |
125 | You may copy and distribute the Document in any medium, either
126 | commercially or noncommercially, provided that this License, the
127 | copyright notices, and the license notice saying this License applies
128 | to the Document are reproduced in all copies, and that you add no
129 | other conditions whatsoever to those of this License. You may not use
130 | technical measures to obstruct or control the reading or further
131 | copying of the copies you make or distribute. However, you may accept
132 | compensation in exchange for copies. If you distribute a large enough
133 | number of copies you must also follow the conditions in section 3.
134 |
135 | You may also lend copies, under the same conditions stated above, and
136 | you may publicly display copies.
137 |
138 | #### 3. COPYING IN QUANTITY
139 |
140 | If you publish printed copies (or copies in media that commonly have
141 | printed covers) of the Document, numbering more than 100, and the
142 | Document's license notice requires Cover Texts, you must enclose the
143 | copies in covers that carry, clearly and legibly, all these Cover
144 | Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
145 | the back cover. Both covers must also clearly and legibly identify you
146 | as the publisher of these copies. The front cover must present the
147 | full title with all words of the title equally prominent and visible.
148 | You may add other material on the covers in addition. Copying with
149 | changes limited to the covers, as long as they preserve the title of
150 | the Document and satisfy these conditions, can be treated as verbatim
151 | copying in other respects.
152 |
153 | If the required texts for either cover are too voluminous to fit
154 | legibly, you should put the first ones listed (as many as fit
155 | reasonably) on the actual cover, and continue the rest onto adjacent
156 | pages.
157 |
158 | If you publish or distribute Opaque copies of the Document numbering
159 | more than 100, you must either include a machine-readable Transparent
160 | copy along with each Opaque copy, or state in or with each Opaque copy
161 | a computer-network location from which the general network-using
162 | public has access to download using public-standard network protocols
163 | a complete Transparent copy of the Document, free of added material.
164 | If you use the latter option, you must take reasonably prudent steps,
165 | when you begin distribution of Opaque copies in quantity, to ensure
166 | that this Transparent copy will remain thus accessible at the stated
167 | location until at least one year after the last time you distribute an
168 | Opaque copy (directly or through your agents or retailers) of that
169 | edition to the public.
170 |
171 | It is requested, but not required, that you contact the authors of the
172 | Document well before redistributing any large number of copies, to
173 | give them a chance to provide you with an updated version of the
174 | Document.
175 |
176 | #### 4. MODIFICATIONS
177 |
178 | You may copy and distribute a Modified Version of the Document under
179 | the conditions of sections 2 and 3 above, provided that you release
180 | the Modified Version under precisely this License, with the Modified
181 | Version filling the role of the Document, thus licensing distribution
182 | and modification of the Modified Version to whoever possesses a copy
183 | of it. In addition, you must do these things in the Modified Version:
184 |
185 | - A. Use in the Title Page (and on the covers, if any) a title
186 | distinct from that of the Document, and from those of previous
187 | versions (which should, if there were any, be listed in the
188 | History section of the Document). You may use the same title as a
189 | previous version if the original publisher of that version
190 | gives permission.
191 | - B. List on the Title Page, as authors, one or more persons or
192 | entities responsible for authorship of the modifications in the
193 | Modified Version, together with at least five of the principal
194 | authors of the Document (all of its principal authors, if it has
195 | fewer than five), unless they release you from this requirement.
196 | - C. State on the Title page the name of the publisher of the
197 | Modified Version, as the publisher.
198 | - D. Preserve all the copyright notices of the Document.
199 | - E. Add an appropriate copyright notice for your modifications
200 | adjacent to the other copyright notices.
201 | - F. Include, immediately after the copyright notices, a license
202 | notice giving the public permission to use the Modified Version
203 | under the terms of this License, in the form shown in the
204 | Addendum below.
205 | - G. Preserve in that license notice the full lists of Invariant
206 | Sections and required Cover Texts given in the Document's
207 | license notice.
208 | - H. Include an unaltered copy of this License.
209 | - I. Preserve the section Entitled "History", Preserve its Title,
210 | and add to it an item stating at least the title, year, new
211 | authors, and publisher of the Modified Version as given on the
212 | Title Page. If there is no section Entitled "History" in the
213 | Document, create one stating the title, year, authors, and
214 | publisher of the Document as given on its Title Page, then add an
215 | item describing the Modified Version as stated in the
216 | previous sentence.
217 | - J. Preserve the network location, if any, given in the Document
218 | for public access to a Transparent copy of the Document, and
219 | likewise the network locations given in the Document for previous
220 | versions it was based on. These may be placed in the "History"
221 | section. You may omit a network location for a work that was
222 | published at least four years before the Document itself, or if
223 | the original publisher of the version it refers to
224 | gives permission.
225 | - K. For any section Entitled "Acknowledgements" or "Dedications",
226 | Preserve the Title of the section, and preserve in the section all
227 | the substance and tone of each of the contributor acknowledgements
228 | and/or dedications given therein.
229 | - L. Preserve all the Invariant Sections of the Document, unaltered
230 | in their text and in their titles. Section numbers or the
231 | equivalent are not considered part of the section titles.
232 | - M. Delete any section Entitled "Endorsements". Such a section may
233 | not be included in the Modified Version.
234 | - N. Do not retitle any existing section to be Entitled
235 | "Endorsements" or to conflict in title with any Invariant Section.
236 | - O. Preserve any Warranty Disclaimers.
237 |
238 | If the Modified Version includes new front-matter sections or
239 | appendices that qualify as Secondary Sections and contain no material
240 | copied from the Document, you may at your option designate some or all
241 | of these sections as invariant. To do this, add their titles to the
242 | list of Invariant Sections in the Modified Version's license notice.
243 | These titles must be distinct from any other section titles.
244 |
245 | You may add a section Entitled "Endorsements", provided it contains
246 | nothing but endorsements of your Modified Version by various
247 | parties—for example, statements of peer review or that the text has
248 | been approved by an organization as the authoritative definition of a
249 | standard.
250 |
251 | You may add a passage of up to five words as a Front-Cover Text, and a
252 | passage of up to 25 words as a Back-Cover Text, to the end of the list
253 | of Cover Texts in the Modified Version. Only one passage of
254 | Front-Cover Text and one of Back-Cover Text may be added by (or
255 | through arrangements made by) any one entity. If the Document already
256 | includes a cover text for the same cover, previously added by you or
257 | by arrangement made by the same entity you are acting on behalf of,
258 | you may not add another; but you may replace the old one, on explicit
259 | permission from the previous publisher that added the old one.
260 |
261 | The author(s) and publisher(s) of the Document do not by this License
262 | give permission to use their names for publicity for or to assert or
263 | imply endorsement of any Modified Version.
264 |
265 | #### 5. COMBINING DOCUMENTS
266 |
267 | You may combine the Document with other documents released under this
268 | License, under the terms defined in section 4 above for modified
269 | versions, provided that you include in the combination all of the
270 | Invariant Sections of all of the original documents, unmodified, and
271 | list them all as Invariant Sections of your combined work in its
272 | license notice, and that you preserve all their Warranty Disclaimers.
273 |
274 | The combined work need only contain one copy of this License, and
275 | multiple identical Invariant Sections may be replaced with a single
276 | copy. If there are multiple Invariant Sections with the same name but
277 | different contents, make the title of each such section unique by
278 | adding at the end of it, in parentheses, the name of the original
279 | author or publisher of that section if known, or else a unique number.
280 | Make the same adjustment to the section titles in the list of
281 | Invariant Sections in the license notice of the combined work.
282 |
283 | In the combination, you must combine any sections Entitled "History"
284 | in the various original documents, forming one section Entitled
285 | "History"; likewise combine any sections Entitled "Acknowledgements",
286 | and any sections Entitled "Dedications". You must delete all sections
287 | Entitled "Endorsements".
288 |
289 | #### 6. COLLECTIONS OF DOCUMENTS
290 |
291 | You may make a collection consisting of the Document and other
292 | documents released under this License, and replace the individual
293 | copies of this License in the various documents with a single copy
294 | that is included in the collection, provided that you follow the rules
295 | of this License for verbatim copying of each of the documents in all
296 | other respects.
297 |
298 | You may extract a single document from such a collection, and
299 | distribute it individually under this License, provided you insert a
300 | copy of this License into the extracted document, and follow this
301 | License in all other respects regarding verbatim copying of that
302 | document.
303 |
304 | #### 7. AGGREGATION WITH INDEPENDENT WORKS
305 |
306 | A compilation of the Document or its derivatives with other separate
307 | and independent documents or works, in or on a volume of a storage or
308 | distribution medium, is called an "aggregate" if the copyright
309 | resulting from the compilation is not used to limit the legal rights
310 | of the compilation's users beyond what the individual works permit.
311 | When the Document is included in an aggregate, this License does not
312 | apply to the other works in the aggregate which are not themselves
313 | derivative works of the Document.
314 |
315 | If the Cover Text requirement of section 3 is applicable to these
316 | copies of the Document, then if the Document is less than one half of
317 | the entire aggregate, the Document's Cover Texts may be placed on
318 | covers that bracket the Document within the aggregate, or the
319 | electronic equivalent of covers if the Document is in electronic form.
320 | Otherwise they must appear on printed covers that bracket the whole
321 | aggregate.
322 |
323 | #### 8. TRANSLATION
324 |
325 | Translation is considered a kind of modification, so you may
326 | distribute translations of the Document under the terms of section 4.
327 | Replacing Invariant Sections with translations requires special
328 | permission from their copyright holders, but you may include
329 | translations of some or all Invariant Sections in addition to the
330 | original versions of these Invariant Sections. You may include a
331 | translation of this License, and all the license notices in the
332 | Document, and any Warranty Disclaimers, provided that you also include
333 | the original English version of this License and the original versions
334 | of those notices and disclaimers. In case of a disagreement between
335 | the translation and the original version of this License or a notice
336 | or disclaimer, the original version will prevail.
337 |
338 | If a section in the Document is Entitled "Acknowledgements",
339 | "Dedications", or "History", the requirement (section 4) to Preserve
340 | its Title (section 1) will typically require changing the actual
341 | title.
342 |
343 | #### 9. TERMINATION
344 |
345 | You may not copy, modify, sublicense, or distribute the Document
346 | except as expressly provided under this License. Any attempt otherwise
347 | to copy, modify, sublicense, or distribute it is void, and will
348 | automatically terminate your rights under this License.
349 |
350 | However, if you cease all violation of this License, then your license
351 | from a particular copyright holder is reinstated (a) provisionally,
352 | unless and until the copyright holder explicitly and finally
353 | terminates your license, and (b) permanently, if the copyright holder
354 | fails to notify you of the violation by some reasonable means prior to
355 | 60 days after the cessation.
356 |
357 | Moreover, your license from a particular copyright holder is
358 | reinstated permanently if the copyright holder notifies you of the
359 | violation by some reasonable means, this is the first time you have
360 | received notice of violation of this License (for any work) from that
361 | copyright holder, and you cure the violation prior to 30 days after
362 | your receipt of the notice.
363 |
364 | Termination of your rights under this section does not terminate the
365 | licenses of parties who have received copies or rights from you under
366 | this License. If your rights have been terminated and not permanently
367 | reinstated, receipt of a copy of some or all of the same material does
368 | not give you any rights to use it.
369 |
370 | #### 10. FUTURE REVISIONS OF THIS LICENSE
371 |
372 | The Free Software Foundation may publish new, revised versions of the
373 | GNU Free Documentation License from time to time. Such new versions
374 | will be similar in spirit to the present version, but may differ in
375 | detail to address new problems or concerns. See
376 | .
377 |
378 | Each version of the License is given a distinguishing version number.
379 | If the Document specifies that a particular numbered version of this
380 | License "or any later version" applies to it, you have the option of
381 | following the terms and conditions either of that specified version or
382 | of any later version that has been published (not as a draft) by the
383 | Free Software Foundation. If the Document does not specify a version
384 | number of this License, you may choose any version ever published (not
385 | as a draft) by the Free Software Foundation. If the Document specifies
386 | that a proxy can decide which future versions of this License can be
387 | used, that proxy's public statement of acceptance of a version
388 | permanently authorizes you to choose that version for the Document.
389 |
390 | #### 11. RELICENSING
391 |
392 | "Massive Multiauthor Collaboration Site" (or "MMC Site") means any
393 | World Wide Web server that publishes copyrightable works and also
394 | provides prominent facilities for anybody to edit those works. A
395 | public wiki that anybody can edit is an example of such a server. A
396 | "Massive Multiauthor Collaboration" (or "MMC") contained in the site
397 | means any set of copyrightable works thus published on the MMC site.
398 |
399 | "CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
400 | license published by Creative Commons Corporation, a not-for-profit
401 | corporation with a principal place of business in San Francisco,
402 | California, as well as future copyleft versions of that license
403 | published by that same organization.
404 |
405 | "Incorporate" means to publish or republish a Document, in whole or in
406 | part, as part of another Document.
407 |
408 | An MMC is "eligible for relicensing" if it is licensed under this
409 | License, and if all works that were first published under this License
410 | somewhere other than this MMC, and subsequently incorporated in whole
411 | or in part into the MMC, (1) had no cover texts or invariant sections,
412 | and (2) were thus incorporated prior to November 1, 2008.
413 |
414 | The operator of an MMC Site may republish an MMC contained in the site
415 | under CC-BY-SA on the same site at any time before August 1, 2009,
416 | provided the MMC is eligible for relicensing.
417 |
418 | ### ADDENDUM: How to use this License for your documents
419 |
420 | To use this License in a document you have written, include a copy of
421 | the License in the document and put the following copyright and
422 | license notices just after the title page:
423 |
424 | Copyright (C) YEAR YOUR NAME.
425 | Permission is granted to copy, distribute and/or modify this document
426 | under the terms of the GNU Free Documentation License, Version 1.3
427 | or any later version published by the Free Software Foundation;
428 | with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
429 | A copy of the license is included in the section entitled "GNU
430 | Free Documentation License".
431 |
432 | If you have Invariant Sections, Front-Cover Texts and Back-Cover
433 | Texts, replace the "with … Texts." line with this:
434 |
435 | with the Invariant Sections being LIST THEIR TITLES, with the
436 | Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
437 |
438 | If you have Invariant Sections without Cover Texts, or some other
439 | combination of the three, merge those two alternatives to suit the
440 | situation.
441 |
442 | If your document contains nontrivial examples of program code, we
443 | recommend releasing these examples in parallel under your choice of
444 | free software license, such as the GNU General Public License, to
445 | permit their use in free software.
446 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lights Off!
2 |
3 | Ever forget to turn the lights off? Now you can:
4 |
5 | - Stop EC2 instances and RDS/Aurora databases temporarily by tagging them with
6 | cron schedules, to cut AWS costs. Schedules (not references to schedules) go
7 | directly in the tags.
8 |
9 | - Trigger AWS Backup with cron schedules in resource tags.
10 |
11 | - Delete expensive infrastructure temporarily by tagging your own
12 | CloudFormation stacks with cron schedules.
13 |
14 | - Easily deploy this tool to multiple AWS accounts and regions.
15 |
16 | Click to view the architecture diagram:
17 |
18 | [
](media/lights-off-aws-architecture-and-flow.png?raw=true "Architecture diagram and flowchart for Lights Off, AWS!")
19 |
20 | > Most of all, this tool is lightweight. Not counting blanks, comments, or
21 | tests, AWS's
22 | [Instance Scheduler](https://github.com/aws-solutions/instance-scheduler-on-aws)
23 | has over 9,500 lines of Python! At about 600 lines of Python, Lights Off is
24 | easy to understand, maintain, and extend.
25 |
26 | Jump to:
27 | [Quick Start](#quick-start)
28 | •
29 | [Tags](#tag-keys-operations)
30 | •
31 | [Schedules](#tag-values-schedules)
32 | •
33 | [Multi-Account, Multi-Region](#multi-account-multi-region-cloudformation-stackset)
34 | •
35 | [Security](#security)
36 |
37 | ## Quick Start
38 |
39 | 1. Log in to the AWS Console as an administrator.
40 |
41 | 2. Tag a running, non-essential
42 | [EC2 instance](https://console.aws.amazon.com/ec2/home#Instances)
43 | with:
44 |
45 | - `sched-stop` : `d=_ H:M=11:30` , replacing 11:30 with the
46 | [current UTC time](https://www.timeanddate.com/worldclock/timezone/utc) +
47 | 20 minutes, rounded upward to :00, :10, :20, :30, :40, or :50.
48 |
49 | 3. Create a
50 | [CloudFormation stack](https://console.aws.amazon.com/cloudformation/home).
51 | Select Upload a template file, then select Choose file and navigate to a
52 | locally-saved copy of
53 | [lights_off_aws.yaml](/cloudformation/lights_off_aws.yaml?raw=true)
54 | [right-click to save as...]. On the next page, set:
55 |
56 | - Stack name: `LightsOff`
57 |
58 |
59 |
60 | If stack creation fails with an UnreservedConcurrentExecution error...
61 |
62 | Request that
63 | [Service Quotas → AWS services → AWS Lambda → Concurrent executions](https://console.aws.amazon.com/servicequotas/home/services/lambda/quotas/L-B99A9384)
64 | be increased. The default is `1000` .
65 |
66 | Lights Off needs 1 unit for a time-critical function. New AWS accounts
67 | start with a quota of 10, but Lambda always holds back 10, which leaves 0
68 | available! Within a given AWS account, the quota is set separately for
69 | each region.
70 |
71 |
72 |
73 | 4. After about 20 minutes, check whether the EC2 instance is stopped. Restart
74 | it and delete the `sched-stop` tag.
75 |
76 | Jump to:
77 | [Extra Setup](#extra-setup)
78 | •
79 | [Multi-Account, Multi-Region](#multi-account-multi-region-cloudformation-stackset)
80 |
81 | ## Tag Keys (Operations)
82 |
83 | ||`sched-stop`|`sched-hibernate`|`sched-backup`|
84 | |:---|:---:|:---:|:---:|
85 | ||**`sched-start`**|||
86 | |EC2:||||
87 | |[Instance](https://console.aws.amazon.com/ec2/home#Instances)|[✓](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Stop_Start.html)|[✓](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/hibernating-prerequisites.html)|→ Image (AMI)|
88 | |[EBS Volume](https://console.aws.amazon.com/ec2/home#Volumes)|||→ Snapshot|
89 | |RDS and Aurora:||||
90 | |[Database Instance](https://console.aws.amazon.com/rds/home#databases:)|[✓](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_StopInstance.html)||→ Snapshot|
91 | |[Database Cluster](https://console.aws.amazon.com/rds/home#databases:)|[✓](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-cluster-stop-start.html)||→ Snapshot|
92 |
93 | > Whether a database operation is at the cluster or instance level depends on
94 | your choice of Aurora or RDS, and for RDS, on your database's configuration.
95 |
96 | ## Tag Values (Schedules)
97 |
98 | ### Work Week Examples
99 |
100 | These cover Monday to Friday daytime work hours, 07:30 to 19:30, year-round
101 | (see
102 | [time zone converter](https://www.timeanddate.com/worldclock/converter.html?p1=1440&p2=103&p3=224&p4=75&p5=64&p6=179&p7=175&p8=136&p9=133&p10=195&p11=367&p12=54)).
103 |
104 | |Locations|Hours Saved|`sched-start`|`sched-stop`|
105 | |:---|:---:|:---:|:---:|
106 | |USA Mainland|**> 50%**|`u=1 u=2 u=3 u=4 u=5 H:M=11:30`|`u=2 u=3 u=4 u=5 u=6 H:M=03:30`|
107 | |North America (Hawaii to Newfoundland)|**> 40%**|`u=1 u=2 u=3 u=4 u=5 H:M=10:00`|`u=2 u=3 u=4 u=5 u=6 H:M=05:30`|
108 | |Europe|**> 50%**|`u=1 u=2 u=3 u=4 u=5 H:M=04:30`|`u=1 u=2 u=3 u=4 u=5 H:M=19:30`|
109 | |India|**> 60%**|`u=1 u=2 u=3 u=4 u=5 H:M=02:00`|`u=1 u=2 u=3 u=4 u=5 H:M=14:00`|
110 | |North America + Europe|**> 20%**|`u=1 H:M=04:30`|`u=6 H:M=05:30`|
111 | |North America + Europe + India|**> 20%**|`u=1 H:M=02:00`|`u=6 H:M=05:30`|
112 | |Europe + India|**> 40%**|`u=1 u=2 u=3 u=4 u=5 H:M=02:00`|`u=1 u=2 u=3 u=4 u=5 H:M=19:30`|
113 |
114 | #### Stopping an RDS or Aurora Database Longer than 7 Days
115 |
116 |
117 | Solutions for stopping a database indefinitely...
118 |
119 | RDS and Aurora automatically start stopped databases after 7 days.
120 |
121 | For a work-around, check out
122 | [github.com/sqlxpert/stay-stopped-aws-rds-aurora](https://github.com/sqlxpert/stay-stopped-aws-rds-aurora/#stay-stopped-rds-and-aurora)
123 | , or modify of the `sched-stop` schedules above and set a once-a-week
124 | `sched-start` schedule that leaves enough time for the database to finish
125 | starting before it will be stopped again. The database will be started, and
126 | stopped again, at least once every 7 days.
127 |
128 | - Change the `sched-start` time to an earlier time if the database routinely
129 | takes more than 1 hour to start. (You may also have to change the weekday to
130 | be 1 day earlier.)
131 | - One or two days are added to `sched-stop` so that a slow-starting database
132 | will receive an extra stop attempt 24 hours after the first attempt (and,
133 | where possible, 48 hours after, as well). Stopping a database that has
134 | already been stopped is harmless.
135 | - For North America + Europe or North America + Europe + India, if you start
136 | the database manually, be sure to stop it manually when you are finished
137 | using it. Elsewhere, it will be stopped at the end of the usual work day.
138 | - To keep up with updates, it is a good practice to set a database's weekly
139 | maintenance window to a time period when the database will be running.
140 |
141 | |Locations|`sched-start`|`sched-stop`|
142 | |:---|:---:|:---:|
143 | |USA Mainland|`uTH:M=6T02:30`|`d=_ H:M=03:30`|
144 | |North America (Hawaii to Newfoundland)|`uTH:M=6T04:30`|`d=_ H:M=05:30`|
145 | |Europe|`uTH:M=5T18:30`|`d=_ H:M=19:30`|
146 | |India|`uTH:M=5T13:00`|`d=_ H:M=14:00`|
147 | |North America + Europe|`uTH:M=6T04:30`|`u=6 u=7 H:M=05:30`|
148 | |North America + Europe + India|`uTH:M=6T04:30`|`u=6 u=7 H:M=05:30`|
149 | |Europe + India|`uTH:M=5T18:30`|`d=_ H:M=19:30`|
150 |
151 |
152 |
153 | ### Rules
154 |
155 | - Coordinated Universal Time (UTC)
156 | - 24-hour clock
157 | - Days before times, hours before minutes
158 | - The day, the hour and the minute must all be resolved
159 | - Multiple operations on the same resource at the same time are _all_ canceled
160 |
161 | Space was chosen as the separator and underscore, as the wildcard, because
162 | [RDS does not allow commas or asterisks](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Tagging.html#Overview.Tagging).
163 |
164 | ### Single Terms
165 |
166 | |Type|Literal Values ([strftime](http://manpages.ubuntu.com/manpages/noble/man3/strftime.3.html#description))|Wildcard|
167 | |:---|:---:|:---:|
168 | |Day of month|`d=01` ... `d=31`|`d=_`|
169 | |Day of week ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Week_dates))|`u=1` (Monday) ... `u=7` (Sunday)||
170 | |Hour|`H=00` ... `H=23`|`H=_`|
171 | |Minute (multiple of 10)|`M=00` , `M=10` , `M=20` , `M=30` , `M=40` , `M=50`||
172 |
173 | ### Compound Terms
174 |
175 | |Type|Note|Literal Values|
176 | |:---|:---:|:---:|
177 | |Once a day|d=_ or d=_NN_ or u=_N_ first!|`H:M=00:00` ... `H:M=23:50`|
178 | |Once a week||`uTH:M=1T00:00` ... `uTH:M=7T23:50`|
179 | |Once a month||`dTH:M=01T00:00` ... `dTH:M=31T23:50`|
180 |
181 | ### Backup Examples
182 |
183 | |`sched-backup`|Description|
184 | |:---:|:---:|
185 | |`d=01 d=15 H=03 H=19 M=00`|Traditional cron: 1st and 15th days of the month, at 03:00 and 19:00|
186 | |`d=_ H:M=03:00 H=_ M=15 M=45`|Every day, at 03:00 _plus_ every hour at 15 and 45 minutes after the hour|
187 | |`dTH:M=01T00:00`|Start of month _(instead of end of month)_|
188 | |`dTH:M=01T03:00 uTH:M=5T19:00 d=_ H=11 M=15`|1st day of the month at 03:00, _plus_ Friday at 19:00, _plus_ every day at 11:15|
189 |
190 | ## Extra Setup
191 |
192 | ### Starting EC2 Instances with Encrypted EBS Volumes
193 |
194 | In most cases, you can use the `sched-start` tag without setup.
195 |
196 |
197 | If you use custom KMS encryption keys from a different AWS account...
198 |
199 | The `sched-start` tag works for EC2 instances with EBS volumes if:
200 |
201 | - Your EBS volumes are unencrypted, or
202 | - You use the default, AWS-managed `aws/ebs` encryption key, or
203 | - You use custom keys in the same AWS account as each EC2 instance, the key
204 | policies contain the default `"Enable IAM User Permissions"` statement, and
205 | they do not contain `"Deny"` statements.
206 |
207 | Because your custom keys are in a different AWS account than your EC2
208 | instances, you must add a statement like the following to the key policies:
209 |
210 | ```json
211 | {
212 | "Sid": "LightsOffEc2StartInstancesWithEncryptedEbsVolumes",
213 | "Effect": "Allow",
214 | "Principal": "*",
215 | "Action": "kms:CreateGrant",
216 | "Resource": "*",
217 | "Condition": {
218 | "ForAnyValue:StringLike": {
219 | "aws:PrincipalOrgPaths": "o-ORG_ID/r-ROOT_ID/ou-PARENT_ORG_UNIT_ID/*"
220 | },
221 | "ArnLike": {
222 | "aws:PrincipalArn": "arn:aws:iam::ACCOUNT:role/*LightsOff*-DoLambdaFnRole-*"
223 | },
224 | "StringLike": {
225 | "kms:ViaService": "ec2.*.amazonaws.com"
226 | },
227 | "Bool": {
228 | "kms:GrantIsForAWSResource": "true"
229 | }
230 | }
231 | }
232 | ```
233 |
234 | - One account: Delete the entire `"ForAnyValue:StringLike"` section and
235 | replace _ACCOUNT_ with the account number of the AWS account in which you
236 | have installed Lights Off.
237 |
238 | - AWS Organizations: Replace _ACCOUNT_ with `*` and _o-ORG_ID_ , _r-ROOT_ID_ ,
239 | and _ou-PARENT_ORG_UNIT_ID_ with the identifiers of your organization, your
240 | organization root, and the organizational unit in which you have installed
241 | Lights Off. `/*` at the end of this organization path stands for child OUs,
242 | if any. Do not use a path less specific than `"o-ORG_ID/*"` .
243 |
244 | > If an EC2 instance does not start as scheduled, a KMS key permissions error
245 | is possible.
246 |
247 |
248 |
249 | ### Making Backups
250 |
251 | You can use the `sched-backup` tag with minimal setup if you work in a small
252 | number of regions and/or AWS accounts. Use the AWS Console to view the
253 | [list of AWS Backup vaults](https://console.aws.amazon.com/backup/home#/backupvaults)
254 | one time in each AWS account and region. Make one backup in each AWS account
255 | ([AWS Backup](https://console.aws.amazon.com/backup/home#) → My account
256 | → Dashboard → On-demand backup). If you use _custom_ KMS keys, they
257 | must be in the same AWS account as the disks and databases encrypted with
258 | them.
259 |
260 |
261 | If you work across many regions and/or AWS accounts...
262 |
263 | Because you want to use the `sched-backup` tag in a complex AWS environment,
264 | you must address the following AWS Backup requirements:
265 |
266 | 1. Vault
267 |
268 | AWS Backup creates the `Default` vault the first time you open the
269 | [list of vaults](https://console.aws.amazon.com/backup/home#/backupvaults)
270 | in a given AWS account and region, using the AWS Console. Otherwise, see
271 | [Backup vault creation](https://docs.aws.amazon.com/aws-backup/latest/devguide/create-a-vault.html)
272 | and
273 | [AWS::Backup::BackupVault](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-backup-backupvault.html)
274 | or
275 | [aws_backup_vault](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault)
276 | . Update the `BackupVaultName` CloudFormation stack parameter if necessary.
277 |
278 | 2. Vault policy
279 |
280 | If you have added `"Deny"` statements, be sure that `DoLambdaFnRole` still
281 | has access.
282 |
283 | 3. Backup role
284 |
285 | AWS Backup creates `AWSBackupDefaultServiceRole` the first time you make a
286 | backup in a given AWS account using the AWS Console
287 | ([AWS Backup](https://console.aws.amazon.com/backup/home#) → My
288 | account → Dashboard → On-demand backup). Otherwise, see
289 | [Default service role for AWS Backup](https://docs.aws.amazon.com/aws-backup/latest/devguide/iam-service-roles.html#default-service-roles).
290 | Update `BackupRoleName` in CloudFormation if necessary.
291 |
292 | 4. KMS key policies
293 |
294 | `AWSBackupDefaultServiceRole` works if:
295 |
296 | - Your EBS volumes and RDS/Aurora databases are unencrypted, or
297 | - You use the default, AWS-managed `aws/ebs` and `aws/rds` encryption keys, or
298 | - You use custom keys in the same AWS account as each disk and database,
299 | the key policies contain the default `"Enable IAM User Permissions"`
300 | statement, and they do not contain `"Deny"` statements.
301 |
302 | If your custom keys are in a different AWS account than your disks and
303 | databases, you must modify the key policies. See
304 | [Encryption for backups in AWS Backup](https://docs.aws.amazon.com/aws-backup/latest/devguide/encryption.html),
305 | [How EBS uses KMS](https://docs.aws.amazon.com/kms/latest/developerguide/services-ebs.html),
306 | [Overview of encrypting RDS resources](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html#Overview.Encryption.Overview),
307 | and
308 | [Key policies in KMS](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html).
309 |
310 | > If no backup jobs appear in AWS Backup, or if jobs do not start, a
311 | permissions problem is likely.
312 |
313 |
314 |
315 | ### Hidden Policies
316 |
317 | Service and resource control policies (SCPs and RCPs), permissions boundaries,
318 | and session policies can interfere with the installation or usage of Lights
319 | Off. Check with your AWS administrator!
320 |
321 | ## Accessing Backups
322 |
323 | |Goal|Services|
324 | |:---|:---:|
325 | |List backups|AWS Backup|
326 | |View underlying images and/or snapshots|EC2 and RDS|
327 | |Restore (create new resources from) backups|EC2 and RDS, or AWS Backup|
328 | |Delete backups|AWS Backup|
329 |
330 | AWS Backup copies resource tags to backups. Lights Off adds `sched-time` to
331 | indicate when the backup was _scheduled_ to occur, in
332 | [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations)
333 | basic format (example: `20241231T1400Z`).
334 |
335 | ## On/Off Switch
336 |
337 | - You can toggle the `Enable` parameter of your Lights Off CloudFormation
338 | stack.
339 | - While Enable is `false`, scheduled operations do not happen; they are
340 | skipped permanently.
341 |
342 | ## Logging
343 |
344 | - Check the
345 | [LightsOff CloudWatch log groups](https://console.aws.amazon.com/cloudwatch/home#logsV2:log-groups$3FlogGroupNameFilter$3DLightsOff-).
346 | - Log entries are JSON objects.
347 | - Lights Off includes `"level"` , `"type"` and `"value"` keys.
348 | - Other software components may use different keys.
349 | - For more data, change the `LogLevel` in CloudFormation.
350 | - Scrutinize log entries at the `ERROR` level.
351 | - Both logs:
352 | Entries with the `"stackTrace"` key represent unexpected exceptions that
353 | require correction. These are unusual.
354 | - "Find" log:
355 | All other entries at the `ERROR` level require correction.
356 | - "Do" log:
357 | Some other entries at the `ERROR` level do not require correction.
358 |
359 | What to consider when evaluating errors...
360 |
361 | The state of an AWS resource might change between the "Find" and "Do"
362 | steps; this sequence is fundamentally non-atomic. An operation might
363 | also be repeated due to queue message delivery logic; operations are
364 | idempotent. If a state change is favorable or an operation is repeated,
365 | Lights Off logs success responses or expected exceptions (depending on
366 | the AWS service) at the `INFO` level. For RDS database _instance_
367 | start/stop operations, however, Lights Off logs expected exceptions at
368 | the `ERROR` level because it cannot determine whether they represent
369 | actual errors or harmless repetition (such as trying to start a
370 | database instance that has already been started).
371 |
372 | For complete details, see the technical article
373 | [Idempotence: Doing It More than Once](https://sqlxpert.github.io/2025/05/17/idempotence-doing-it-more-than-once.html).
374 |
375 |
376 | - Check the `ErrorQueue`
377 | [SQS queue](https://console.aws.amazon.com/sqs/v3/home#/queues)
378 | for "Find" and "Do" events that were not delivered, or not fully processed.
379 | - Check CloudTrail for the final stages of `sched-start` and `sched-backup`
380 | operations.
381 |
382 | ## Advanced Installation
383 |
384 | ### Multi-Account, Multi-Region (CloudFormation StackSet)
385 |
386 | For reliability, Lights Off works completely independently in each AWS
387 | account+region combination. To deploy to multiple regions and/or AWS accounts,
388 |
389 | 1. Delete any standalone Lights Off CloudFormation _stacks_ in the target AWS
390 | accounts and regions.
391 |
392 | 2. Complete the prerequisites for creating a _StackSet_ with
393 | [service-managed permissions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-orgs-enable-trusted-access.html).
394 |
395 | 3. Make sure that the AWS Lambda `Concurrent executions` quota is sufficient
396 | in every target AWS account, in every target region. See the note at the
397 | end of [Quick Start](#quick-start) Step 3.
398 |
399 | 4. In the management AWS account (or a delegated administrator account),
400 | create a
401 | [CloudFormation StackSet](https://console.aws.amazon.com/cloudformation/home#/stacksets).
402 | Select Upload a template file, then select Choose file and upload a
403 | locally-saved copy of
404 | [lights_off_aws.yaml](/cloudformation/lights_off_aws.yaml?raw=true)
405 | [right-click to save as...]. On the next page, set:
406 |
407 | - StackSet name: `LightsOff`
408 |
409 | 5. Two pages later, under Deployment targets, select Deploy to Organizational
410 | Units (OUs). Enter the AWS OU ID of the target Organizational Unit. Lights
411 | Off will be deployed to all AWS accounts within this Organizational Unit.
412 | Toward the bottom of the page, specify the target regions.
413 |
414 | ### Least-Privilege Installation
415 |
416 |
417 | Least-privilege installation details...
418 |
419 | You can use a
420 | [CloudFormation service role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html)
421 | to delegate only the privileges needed to create the Lights Off stack. First,
422 | create the `LightsOffPrereq` stack from
423 | [lights_off_aws_prereq.yaml](/cloudformation/lights_off_aws_prereq.yaml?raw=true)
424 | . Next, when you create the `LightsOff` stack from
425 | [lights_off_aws.yaml](/cloudformation/lights_off_aws.yaml?raw=true) , set IAM
426 | role - optional to `LightsOffPrereq-DeploymentRole` . If your own privileges
427 | are limited, you might need permission to pass the deployment role to
428 | CloudFormation. See the `LightsOffPrereq-SampleDeploymentRolePassRolePol` IAM
429 | policy for an example.
430 |
431 | For a CloudFormation StackSet, you can use
432 | [self-managed permissions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html)
433 | by copying the inline IAM policy of `LightsOffPrereq-DeploymentRole` to a
434 | customer-managed IAM policy, attaching your policy to
435 | `AWSCloudFormationStackSetExecutionRole` and propagating the policy and the
436 | role policy attachment to all target AWS accounts.
437 |
438 |
439 | ### Installation with Terraform
440 |
441 | Terraform users are often willing to wrap a CloudFormation stack in HashiCorp
442 | Configuration Language, because AWS supplies tools in the form of
443 | CloudFormation templates. See
444 | [aws_cloudformation_stack](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack)
445 | .
446 |
447 | Wrapping a CloudFormation StackSet in HCL is much easier than configuring and
448 | using Terraform to deploy and maintain identical resources in multiple regions
449 | and/or AWS accounts. See
450 | [aws_cloudformation_stack_set](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set)
451 | .
452 |
453 | ## Security
454 |
455 | > In accordance with the software license, nothing in this section creates a
456 | warranty, an indemnification, an assumption of liability, etc. Use this
457 | software at your own risk. You are encouraged to evaluate the source code.
458 |
459 |
460 | Security details...
461 |
462 | ### Security Design Goals
463 |
464 | - Least-privilege roles for the AWS Lambda functions that find resources and
465 | do scheduled operations. The "Do" function is authorized to perform a small
466 | set of operations, and at that, only when a resource has the correct tag
467 | key. (AWS Backup creates backups, using a role that you can configure.)
468 |
469 | - A least-privilege queue policy. The operation queue can only consume
470 | messages from the "Find" function and produce messages for the "Do" function
471 | (or an error queue, if an operation fails). Encryption in transit is
472 | required.
473 |
474 | - Readable IAM policies, formatted as CloudFormation YAML rather than JSON,
475 | and broken down into discrete statements by service, resource or principal.
476 |
477 | - Optional encryption at rest with the AWS Key Management System (KMS), for
478 | queue message bodies (may contain resource identifiers) and for logs (may
479 | contain resource metadata).
480 |
481 | - No data storage other than in queues and logs, with short or configurable
482 | retention periods.
483 |
484 | - Tolerance for clock drift in a distributed system. The "Find" function
485 | starts 1 minute into the 10-minute cycle and operation queue entries expire
486 | 9 minutes in.
487 |
488 | - An optional CloudFormation service role for least-privilege deployment.
489 |
490 | ### Security Steps You Can Take
491 |
492 | - Only allow trusted people and services to tag AWS resources. You can
493 | deny the right to add, change and delete `sched-` tags by including the
494 | [aws:TagKeys condition key](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html#access_tags_control-tag-keys)
495 | in a permissions boundary.
496 |
497 | - Prevent people who can set the `sched-backup` tag from deleting backups.
498 |
499 | - Prevent people from modifying components, most of which can be identified by
500 | `LightsOff` in ARNs and in the automatic `aws:cloudformation:stack-name`
501 | tag. Limiting permissions so that the deployment role is _necessary_ for
502 | stack modifications is ideal.
503 |
504 | - Prevent people from directly invoking the AWS Lambda functions and from
505 | passing the function roles to arbitrary functions.
506 |
507 | - Log infrastructure changes using AWS CloudTrail, and set up alerts.
508 |
509 | - Automatically copy backups to an AWS Backup vault in an isolated account.
510 |
511 | - Separate production workloads. You might choose not to deploy Lights Off to
512 | AWS accounts used for production, or you might add a custom policy to the
513 | "Do" function's role, denying authority to stop production resources (
514 | `AttachLocalPolicy` in CloudFormation).
515 |
516 |
517 |
518 | ## Advice
519 |
520 | - Test Lights Off in your AWS environment. Please
521 | [report bugs](https://github.com/sqlxpert/lights-off-aws/issues).
522 |
523 | - Test your backups! Are they finishing on-schedule? Can they be restored?
524 | [AWS Backup restore testing](https://docs.aws.amazon.com/aws-backup/latest/devguide/restore-testing.html)
525 | can help.
526 |
527 | - Be aware: of charges for AWS Lambda functions, SQS queues, CloudWatch Logs,
528 | KMS, backup storage, and early deletion from cold storage; of the minimum
529 | charge when you stop an EC2 instance or RDS database with a commercial
530 | license; of the resumption of charges when RDS or Aurora restarts a stopped
531 | database after 7 days; and of ongoing storage charges while EC2 instances
532 | and RDS/Aurora databases are stopped. Have we missed anything?
533 |
534 | ## Bonus: Delete and Recreate Expensive Resources on a Schedule
535 |
536 |
537 | Scheduled CloudFormation stack update details...
538 |
539 | Lights Off can delete and recreate many types of expensive AWS infrastructure
540 | in your own CloudFormation stacks, based on cron schedules in stack tags.
541 |
542 | Deleting AWS Client VPN resources overnight, while developers are asleep, is
543 | a sample use case. See
544 | [10-minute AWS Client VPN](https://github.com/sqlxpert/10-minute-aws-client-vpn#automatic-scheduling).
545 |
546 | To make your own CloudFormation template compatible, see
547 | [lights_off_aws_bonus_cloudformation_example.yaml](/cloudformation/lights_off_aws_bonus_cloudformation_example.yaml)
548 | .
549 |
550 | Not every resource needs to be deleted and recreated; condition the creation
551 | of _expensive_ resources on the `Enable` parameter. In the AWS Client VPN
552 | stack, the VPN endpoints and VPC security groups are not deleted, because they
553 | do not cost anything. The VPN attachments can be deleted and recreated with no
554 | need to reconfigure VPN clients.
555 |
556 | Set the `sched-set-Enable-true` and `sched-set-Enable-false` tags on
557 | your own CloudFormation stack and make sure that the
558 | `EnableSchedCloudFormationOps` parameter of the _LightsOff stack or StackSet_
559 | is set to `true` (the default). At the scheduled times, Lights Off will
560 | perform a stack update, toggling the value of the `Enable` parameter to `true`
561 | or `false`. (Capitalize **E**nable in the tag keys, to match the parameter
562 | name.)
563 |
564 | If your stack's status is other than `CREATE_COMPLETE` or `UPDATE_COMPLETE` at
565 | the scheduled time, Lights Off logs an error of `"type"`
566 | `STACK_STATUS_IRREGULAR` in the "Find" [log](#logging) instead of attempting
567 | an update that is likely to fail and require a rollback. To resume scheduled
568 | stack updates, resolve the underlying template error or permissions error and
569 | successfully complete one manual stack update.
570 |
571 |
572 | ## Extensibility
573 |
574 |
575 | Extensibility details...
576 |
577 | Lights Off takes advantage of patterns in boto3, the AWS software development
578 | kit (SDK) for Python, and in the underlying AWS API. Adding AWS services,
579 | resource types, and operations is easy. For example, supporting RDS database
580 | _clusters_ (RDS database _instances_ were already supported) required adding:
581 |
582 | ```python
583 | AWSRsrcType(
584 | "rds",
585 | ("DB", "Cluster"),
586 | {
587 | ("start", ): {},
588 | ("stop", ): {},
589 | ("backup", ): {},
590 | },
591 | rsrc_id_key_suffix="Identifier",
592 | tags_key="TagList",
593 | )
594 | ```
595 |
596 | Given the words `DB` and `Cluster` in the resource type name, plus the
597 | operation verb `start`, the `sched-start` tag key and the `start_db_cluster`
598 | method name are derived mechanically.
599 |
600 | If an operation method takes more than just the resource identifier, add a
601 | dictionary of static keyword arguments. For complex arguments, sub-class the
602 | `AWSOp` class and override `op_kwargs` .
603 |
604 | The `start_backup_job` method takes an Amazon Resource Name (ARN), whose
605 | format is consistent for all resource types. As long as AWS Backup supports
606 | the resource type, there is no extra work to do.
607 |
608 | Add statements like the one below to the Identity and Access Management (IAM)
609 | policy for the role used by the "Do" AWS Lambda function, to authorize
610 | operations. You must of course authorize the role used by the "Find" function
611 | to describe (list) resources.
612 |
613 | ```yaml
614 | - Effect: Allow
615 | Action: rds:StartDBCluster
616 | Resource: !Sub "arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:cluster:*"
617 | Condition:
618 | StringLike: { "aws:ResourceTag/sched-start": "*" }
619 | ```
620 |
621 | What capabilities would you like to add? Submit a
622 | [pull request](https://github.com/sqlxpert/lights-off-aws/pulls) today!
623 |
624 |
625 | ## Progress
626 |
627 | Paul wrote TagSchedOps, the first version of this tool, before Systems
628 | Manager, Data Lifecycle Manager or AWS Backup existed. The tool remains a
629 | simple alternative to
630 | [Systems Manager Automation runbooks for
631 | stopping EC2 instances](https://docs.aws.amazon.com/systems-manager-automation-runbooks/latest/userguide/automation-aws-stopec2instance.html),
632 | etc. It is now integrated with AWS Backup, leveraging the security and
633 | management benefits (including backup retention lifecycle policies) but
634 | offering a simple alternative to
635 | [backup plans](https://docs.aws.amazon.com/aws-backup/latest/devguide/about-backup-plans.html).
636 |
637 | |Year|AWS Lambda Python Lines|Core CloudFormation YAML Lines|
638 | |:---:|:---:|:---:|
639 | |2017|≈ 775|≈ 2,140|
640 | |2022|630|800 ✓|
641 | |2025|610 ✓|980|
642 |
643 | ## Dedication
644 |
645 | This project is dedicated to ej, Marianne and Régis, Ivan, and to the
646 | wonderful colleagues whom Paul has worked with over the years. Thank you to
647 | Corey for sharing it with the AWS user community in _Last Week in AWS_
648 | newsletter issues
649 | [286 (October 3, 2022)](https://www.lastweekinaws.com/newsletter/amazon-file-cash/#h-tools)
650 | and
651 | 424 (May 27, 2025),
652 | and to Lee for suggesting the new name.
653 |
654 | ## Licenses
655 |
656 | |Scope|Link|Included Copy|
657 | |:---|:---:|:---:|
658 | |Source code files, and source code embedded in documentation files|[GNU General Public License (GPL) 3.0](http://www.gnu.org/licenses/gpl-3.0.html)|[LICENSE-CODE.md](/LICENSE-CODE.md)|
659 | |Documentation files (including this readme file)|[GNU Free Documentation License (FDL) 1.3](http://www.gnu.org/licenses/fdl-1.3.html)|[LICENSE-DOC.md](/LICENSE-DOC.md)|
660 |
661 | Copyright Paul Marcelin
662 |
663 | Contact: `marcelin` at `cmu.edu` (replace "at" with `@`)
664 |
--------------------------------------------------------------------------------
/cloudformation/.cfnlintrc.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Start, stop and back up AWS resources tagged with cron schedules
3 | # github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
4 |
5 | ignore_checks:
6 | - W2510
7 | # W2510: Lambda Memory Size parameters should use MinValue, MaxValue or
8 | # AllowedValues: See comment about "underlying APIs"
9 |
--------------------------------------------------------------------------------
/cloudformation/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Start, stop and back up AWS resources tagged with cron schedules.
3 | # github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
4 |
5 | extends: default
6 |
7 | rules:
8 | braces:
9 | level: warning
10 | max-spaces-inside: 1 # AWS CloudFormation convention
11 | brackets:
12 | level: warning
13 | max-spaces-inside: 1 # AWS CloudFormation convention
14 | line-length:
15 | max: 79
16 | level: warning
17 | allow-non-breakable-words: true
18 | allow-non-breakable-inline-mappings: true
19 | # No suitable option for some unavoidably long lines, like ones containing
20 | # ARNs. Leave warning in place as a reminder for other cases.
21 |
--------------------------------------------------------------------------------
/cloudformation/lights_off_aws_bonus_cloudformation_example.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | AWSTemplateFormatVersion: "2010-09-09"
3 |
4 | Description: |-
5 | Demonstrates deleting and recreating AWS resources in your own
6 | CloudFormation stack, based on cron schedules in stack tags.
7 |
8 | github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
9 |
10 | # STEP 0 #####################################################################
11 | #
12 | # Note that CloudFormation "transforms" are not currently compatible. Search
13 | # for "transforms" in lights_off_aws.py .
14 | #
15 | # STEP 1 #####################################################################
16 | #
17 | # Define a service role that allows CloudFormation to create, tag, update,
18 | # untag, and delete ALL of the resources in your template. See
19 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html
20 | #
21 | # For this example template, create an IAM role in the AWS Console. Leave
22 | # Trusted entity type set to AWS service. From Service or use case, near the
23 | # bottom, select CloudFormation. On the next page, search for the
24 | # AmazonEC2FullAccess policy and check the box to the left of that policy.
25 | # It is not least-privilege, and is only suitable for testing.
26 | #
27 | # You MUST select your deployment role when you create a CloudFormation stack
28 | # from your template. Scroll to the Permissions section and set IAM role -
29 | # optional . Otherwise, stack updates triggered by Lights Off will fail.
30 | #
31 | # About resource tagging: CloudFormation needs permission to get/list, create,
32 | # update, and delete arbitrary resource tags, due to the automatic propagation
33 | # of stack tags. Because CloudFormation is regularly updated to propagate
34 | # stack tags to more resource types, provide tagging privileges for all of the
35 | # resource types in your template, even if CloudFormation doesn't yet
36 | # propagate stack tags to them.
37 | ##############################################################################
38 |
39 | Parameters:
40 |
41 | PlaceholderSuggestedStackName:
42 | Type: String
43 | Default: "LightsOffBonusCloudFormationExample"
44 |
45 | PlaceholderHelp:
46 | Type: String
47 | Default: "github.com/sqlxpert/lights-off-aws#bonus-delete-and-recreate-expensive-resources-on-a-schedule"
48 |
49 | PlaceholderTimeZoneConverter:
50 | Type: String
51 | Default: "www.timeanddate.com/worldclock/timezone/utc"
52 |
53 | PlaceholderSuggestedStackTags:
54 | Type: String
55 | Description: >-
56 | Copy this pair of on/off stack tags to get started. Update the times.
57 | Times must be in UTC. Minute values must be multiples of 10.
58 | Default: "sched-set-Enable-true : d=_ H:M=07:30 , sched-set-Enable-false : d=_ H:M=07:40"
59 |
60 | VpcId:
61 | Type: AWS::EC2::VPC::Id
62 | Description: >-
63 | Identifier of the Virtual Private Cloud in which the sample security
64 | groups will be created
65 |
66 | # STEP 2 ###################################################################
67 | #
68 | # Add this parameter to your template:
69 | Enable: # Do not change the parameter name or the capitalization!
70 | Type: String # No Boolean parameter type is available
71 | Description: >-
72 | Whether to create expensive resources. Lights Off will automatically
73 | update the stack, causing the resources to be created or deleted based
74 | on the schedules in the stack's "sched-set-Enable-true" and
75 | "sched-set-Enable-false" tags. See
76 | https://github.com/sqlxpert/lights-off-aws
77 | AllowedValues:
78 | - "false"
79 | - "true"
80 | Default: "false" # Start without the expensive resources
81 | ##########################################################################
82 |
83 | Metadata: # Optional section, for AWS Console users
84 |
85 | AWS::CloudFormation::Interface:
86 | ParameterGroups: # Orders parameters and groups them into sections
87 | - Label:
88 | default: For Reference
89 | Parameters:
90 | - PlaceholderSuggestedStackName
91 | - PlaceholderHelp
92 | - PlaceholderTimeZoneConverter
93 | - PlaceholderSuggestedStackTags
94 | - Label:
95 | default: Essential
96 | Parameters:
97 | - Enable
98 | - VpcId
99 | ParameterLabels:
100 | PlaceholderSuggestedStackName:
101 | default: Suggested stack name
102 | PlaceholderHelp:
103 | default: For help with this stack, see
104 | PlaceholderTimeZoneConverter:
105 | default: To convert local time to UTC, see
106 | PlaceholderSuggestedStackTags:
107 | default: Suggested stack tags
108 | Enable:
109 | default: Enabled?
110 | VpcId:
111 | default: VPC
112 |
113 | Conditions:
114 |
115 | # STEP 3 ###################################################################
116 | #
117 | # Review
118 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html
119 | # and add this condition to your template:
120 | EnableTrue:
121 | !Equals [!Ref Enable, "true"]
122 | ##########################################################################
123 |
124 | Resources:
125 |
126 | ConditionalSecGrp:
127 | Type: AWS::EC2::SecurityGroup
128 | # STEP 4 #################################################################
129 | #
130 | # Add this property to each conditionally-created resource. There is no
131 | # need to add it to free or low-cost resources.
132 | Condition: EnableTrue
133 | ##########################################################################
134 | Properties:
135 | GroupDescription:
136 | Fn::Sub: >-
137 | Demonstrates deletion and recreation of a resource (this security
138 | group) based on schedules in the
139 | sched-set-Enable-true/-false stack tags of ${AWS::StackName}
140 | Tags:
141 | - Key: Name
142 | Value: !Sub "${AWS::StackName}-ConditionalSecGrp"
143 | VpcId: !Ref VpcId
144 |
145 | SecGrpWithConditionalRule:
146 | Type: AWS::EC2::SecurityGroup
147 | Properties:
148 | GroupDescription:
149 | Fn::Sub: >-
150 | Demonstrates deletion and recreation of a resource property (a
151 | security group ingress rule) based on schedules in the
152 | sched-set-Enable-true/-false stack tags of ${AWS::StackName}
153 | Tags:
154 | - Key: Name
155 | Value: !Sub "${AWS::StackName}-SecGrpWithConditionalRule"
156 | VpcId: !Ref VpcId
157 | SecurityGroupIngress:
158 | - Description: Permanent rule
159 | IpProtocol: tcp
160 | FromPort: 53
161 | ToPort: 53
162 | CidrIp: 8.8.8.8/32
163 | # STEP 5 #############################################################
164 | #
165 | # If you need to conditionally set a resource property or
166 | # conditionally add or remove a list item, use:
167 | # Fn::If: [ EnableTrue, VALUE_IF_ENABLED, VALUE_IF_NOT_ENABLED ]
168 | #
169 | # If a resource property is to be omitted entirely when Enable is
170 | # false, specify !Ref AWS::NoValue for VALUE_IF_NOT_ENABLED .
171 | # See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html#cfn-pseudo-param-novalue
172 | - Fn::If:
173 | - EnableTrue
174 | - Description: Conditional rule
175 | IpProtocol: tcp
176 | FromPort: 53
177 | ToPort: 53
178 | SourceSecurityGroupId: !Ref ConditionalSecGrp
179 | - !Ref AWS::NoValue
180 | # STEP 6 #####################################################################
181 | #
182 | # Test your deployment role and your stack. Try a manual stack update in which
183 | # you change the Enable parameter from false to true, and anoter in which you
184 | # change it from true to false.
185 | #
186 | # STEP 7 #####################################################################
187 | #
188 | # Refer to
189 | # https://github.com/sqlxpert/lights-off-aws#tag-values-schedules .
190 | #
191 | # Make sure that the EnableSchedCloudFormationOps is set to "true" in your
192 | # main LightsOff CloudFormation stack or StackSet.
193 | #
194 | # Add schedule tags to your own stack:
195 | # - sched-set-Enable-true
196 | # - sched-set-Enable-false
197 | #
198 | # Wait for the scheduled times, then check the list of events for your own
199 | # CloudFormation stack. In the AWS Console, the Client request token column
200 | # shows operations ("sched-set-Enable-true" or "sched-set-Enable-false") and
201 | # scheduled times, in ISO 8601 basic form ("20250115T0730Z", for example).
202 | ##############################################################################
203 |
--------------------------------------------------------------------------------
/cloudformation/lights_off_aws_prereq.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | AWSTemplateFormatVersion: "2010-09-09"
3 |
4 | Description: |-
5 | CloudFormation service role to deploy
6 |
7 | github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
8 |
9 | Parameters:
10 |
11 | PlaceholderSuggestedStackName:
12 | Type: String
13 | Default: "LightsOffPrereq"
14 |
15 | PlaceholderHelp:
16 | Type: String
17 | Default: "github.com/sqlxpert/lights-off-aws#least-privilege-installation"
18 |
19 | PlaceholderAdvancedParameters:
20 | Type: String
21 | Default: ""
22 | AllowedValues:
23 | - ""
24 |
25 | StackNameLike:
26 | Type: String
27 | Description: >-
28 | When a stack is created using the deployment role, its name must match
29 | this StringLike/ArnLike pattern. Examples: "LightsOff" only allows a
30 | stack of that name; "LightsOff*" also allows stacks with names such as
31 | "LightsOff2" and "LightsOffTest"; and "StackSet-LightsOff-*" allows a
32 | StackSet named "LightsOff", whose StackSet instances receive names
33 | beginning "StackSet-LightsOff-". See
34 | https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
35 | Default: "LightsOff*"
36 |
37 | Metadata:
38 |
39 | AWS::CloudFormation::Interface:
40 | ParameterGroups:
41 | - Label:
42 | default: For Reference
43 | Parameters:
44 | - PlaceholderSuggestedStackName
45 | - PlaceholderHelp
46 | - Label:
47 | default: Advanced Options
48 | Parameters:
49 | - PlaceholderAdvancedParameters
50 | - Label:
51 | default: For stacks created using the deployment role...
52 | Parameters:
53 | - StackNameLike
54 | ParameterLabels:
55 | PlaceholderHelp:
56 | default: For help with this stack, see
57 | PlaceholderSuggestedStackName:
58 | default: Suggested stack name
59 | PlaceholderAdvancedParameters:
60 | default: Do not change parameters below, unless necessary!
61 | StackNameLike:
62 | default: Allowed stack name pattern
63 |
64 | Resources:
65 |
66 | DeploymentRole:
67 | Type: AWS::IAM::Role
68 | Properties:
69 | Description: >-
70 | Resources in Lights Off CloudFormation stack: create, update, delete
71 | AssumeRolePolicyDocument:
72 | Version: 2012-10-17
73 | Statement:
74 | - Effect: Allow
75 | Principal: { Service: cloudformation.amazonaws.com }
76 | Action: sts:AssumeRole
77 | # In-line policies apply only to this role, which, in turn, can only be
78 | # assumed by CloudFormation. Separate, "managed" policies could be
79 | # attached to other roles or users, allowing permission escalation.
80 | # Administrator should restrict iam:PassRole to prevent use of this role
81 | # with arbitrary CloudFormation stacks.
82 | Policies:
83 | - PolicyName: LightsOffCloudFormationStackDeploy
84 | PolicyDocument:
85 | Version: "2012-10-17"
86 | Statement:
87 |
88 | - Effect: Allow
89 | Action:
90 | - lambda:CreateFunction # Also iam:PassRole
91 | - lambda:GetFunction
92 | - lambda:DeleteFunction
93 | - lambda:UpdateFunctionCode
94 | - lambda:GetFunctionConfiguration
95 | - lambda:UpdateFunctionConfiguration
96 | - lambda:AddPermission
97 | - lambda:RemovePermission
98 | - lambda:PutFunctionConcurrency
99 | - lambda:DeleteFunctionConcurrency
100 | - lambda:TagResource
101 | - lambda:UntagResource
102 | Resource:
103 | - !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:${StackNameLike}-*"
104 | - Effect: Allow
105 | Action:
106 | - lambda:CreateEventSourceMapping
107 | - lambda:GetEventSourceMapping
108 | - lambda:UpdateEventSourceMapping
109 | - lambda:DeleteEventSourceMapping
110 | Resource: "*"
111 | Condition:
112 | ArnLikeIfExists:
113 | "lambda:FunctionArn":
114 | - !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:${StackNameLike}-*"
115 | - Effect: Allow
116 | Action:
117 | - lambda:TagResource
118 | - lambda:UntagResource
119 | Resource:
120 | - !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:event-source-mapping:*"
121 | - Effect: Allow
122 | Action:
123 | - lambda:ListTags
124 | Resource: "*"
125 |
126 | - Effect: Allow
127 | Action:
128 | - logs:CreateLogGroup
129 | - logs:DeleteLogGroup
130 | - logs:PutRetentionPolicy
131 | - logs:DeleteRetentionPolicy
132 | - logs:AssociateKmsKey
133 | - logs:DisassociateKmsKey
134 | - logs:TagLogGroup
135 | - logs:TagResource
136 | - logs:UntagLogGroup
137 | - logs:UntagResource
138 | Resource:
139 | - !Sub "arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:${StackNameLike}-*"
140 | - Effect: Allow
141 | Action:
142 | - logs:DescribeLogGroups
143 | - logs:ListTagsForResource
144 | Resource: "*"
145 |
146 | - Effect: Allow
147 | Action:
148 | - scheduler:CreateScheduleGroup
149 | - scheduler:DeleteScheduleGroup
150 | - scheduler:TagResource
151 | - scheduler:UntagResource
152 | Resource:
153 | - !Sub "arn:${AWS::Partition}:scheduler:*:${AWS::AccountId}:schedule-group/${StackNameLike}-*"
154 | - Effect: Allow
155 | Action:
156 | - scheduler:CreateSchedule # Also iam:PassRole
157 | - scheduler:UpdateSchedule # Also iam:PassRole
158 | - scheduler:DeleteSchedule
159 | # Possible future requirement:
160 | - scheduler:TagResource
161 | - scheduler:UntagResource
162 | Resource:
163 | # Compare CloudWatch Logs and EventBridge Scheduler.
164 | # Right-trimming the stream name from a LogStream ARN gives
165 | # the ARN of the parent LogGroup, but unfortunately,
166 | # right-trimming the schedule name from a schedule ARN does
167 | # not give the ARN of the parent ScheduleGroup.
168 | # arn:PARTITION:logs:REGION:ACCOUNT:log-group:GROUP_NAME:log-stream:STREAM_NAME
169 | # arn:PARTITION:scheduler:REGION:ACCOUNT:schedule-group/GROUP_NAME
170 | # arn:PARTITION:scheduler:REGION:ACCOUNT:schedule/GROUP_NAME/SCHED_NAME
171 | - !Sub "arn:${AWS::Partition}:scheduler:*:${AWS::AccountId}:schedule/${StackNameLike}-*/*"
172 | - Effect: Allow
173 | Action:
174 | - scheduler:ListScheduleGroups
175 | - scheduler:GetScheduleGroup
176 | - scheduler:ListSchedules
177 | - scheduler:GetSchedule
178 | - scheduler:ListTagsForResource
179 | Resource: "*"
180 |
181 | # Left so that existing users can maintain, and eventually,
182 | # delete, the old EventBridge rule (replaced by an EventBridge
183 | # schedule)
184 | - Effect: Allow
185 | Action:
186 | - events:PutRule
187 | - events:DescribeRule
188 | - events:EnableRule
189 | - events:DisableRule
190 | - events:DeleteRule
191 | - events:PutTargets
192 | - events:ListTargetsByRule
193 | - events:RemoveTargets
194 | - events:TagResource
195 | - events:UntagResource
196 | Resource:
197 | - !Sub "arn:${AWS::Partition}:events:*:${AWS::AccountId}:rule/${StackNameLike}-*"
198 | - Effect: Allow
199 | Action:
200 | - events:ListTagsForResource
201 | Resource: "*"
202 |
203 | - Effect: Allow
204 | Action:
205 | - sqs:CreateQueue
206 | - sqs:SetQueueAttributes
207 | - sqs:GetQueueAttributes
208 | - sqs:GetQueueUrl
209 | - sqs:ListDeadLetterSourceQueues
210 | - sqs:DeleteQueue
211 | - sqs:AddPermission
212 | - sqs:RemovePermission
213 | - sqs:TagQueue
214 | - sqs:UntagQueue
215 | Resource:
216 | - !Sub "arn:${AWS::Partition}:sqs:*:${AWS::AccountId}:${StackNameLike}-*"
217 | - Effect: Allow
218 | Action:
219 | - sqs:ListQueues
220 | - sqs:ListQueueTags
221 | Resource: "*"
222 |
223 | - Effect: Allow
224 | Action: iam:PassRole
225 | Resource:
226 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${StackNameLike}-*"
227 | Condition:
228 | StringEquals:
229 | "iam:PassedToService":
230 | - lambda.amazonaws.com
231 | - scheduler.amazonaws.com
232 | ArnLike:
233 | "iam:AssociatedResourceArn":
234 | - !Sub "arn:${AWS::Partition}:lambda:*:${AWS::AccountId}:function:${StackNameLike}-*"
235 | - !Sub "arn:${AWS::Partition}:scheduler:*:${AWS::AccountId}:schedule/${StackNameLike}-*/${StackNameLike}-*"
236 | - Effect: Allow
237 | Action:
238 | - iam:CreateRole
239 | - iam:UpdateRoleDescription
240 | - iam:GetRole
241 | - iam:DeleteRole
242 | - iam:UpdateAssumeRolePolicy
243 | - iam:ListRolePolicies
244 | - iam:GetRolePolicy
245 | - iam:AttachRolePolicy
246 | - iam:DetachRolePolicy
247 | - iam:PutRolePolicy
248 | - iam:DeleteRolePolicy
249 | - iam:ListEntitiesForPolicy
250 | - iam:TagRole
251 | - iam:UntagRole
252 | Resource:
253 | - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${StackNameLike}-*"
254 | - Effect: Allow
255 | Action:
256 | - iam:ListRoles
257 | - iam:ListRoleTags
258 | - iam:ListAttachedRolePolicies
259 | Resource: "*"
260 |
261 | - Effect: Allow
262 | Action:
263 | - kms:ListKeys
264 | - kms:ListAliases
265 | - kms:ListResourceTags
266 | - kms:DescribeKey
267 | Resource: "*"
268 |
269 | SampleDeploymentRolePassRolePol:
270 | Type: "AWS::IAM::ManagedPolicy"
271 | Properties:
272 | Description:
273 | Fn::Sub: >-
274 | ${DeploymentRole}: pass to CloudFormation. Demonstrates a privilege
275 | that non-adminstrators need before they can create a Lights Off
276 | CloudFormation stack using the deployment role.
277 | PolicyDocument:
278 | Version: "2012-10-17"
279 | Statement:
280 | - Effect: Allow
281 | Action: "iam:PassRole"
282 | Resource: !GetAtt DeploymentRole.Arn
283 |
--------------------------------------------------------------------------------
/lights_off_aws.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """Start, stop and back up AWS resources tagged with cron schedules
3 |
4 | github.com/sqlxpert/lights-off-aws GPLv3 Copyright Paul Marcelin
5 | """
6 |
7 | import os
8 | import logging
9 | import datetime
10 | import re
11 | import json
12 | import botocore
13 | import boto3
14 |
15 | logger = logging.getLogger()
16 | # Skip "credentials in environment" INFO message, unavoidable in AWS Lambda:
17 | logging.getLogger("botocore").setLevel(logging.WARNING)
18 |
19 |
20 | def environ_int(environ_var_name):
21 | """Take name of an environment variable, return its integer value
22 | """
23 | return int(os.environ[environ_var_name])
24 |
25 |
26 | SCHED_DELIMS = r"\ +" # Exposed space must be escaped for re.VERBOSE
27 | SCHED_TERMS = rf"([^ ]+{SCHED_DELIMS})*" # Unescaped space inside char class
28 | SCHED_REGEXP_STRFTIME_FMT = rf"""
29 | (^|{SCHED_DELIMS})
30 | (
31 | # Specific monthly or weekly day and time, or...
32 | (dTH:M=%d|uTH:M=%u)T%H:%M
33 | |
34 | # Day wildcard, specific day, or specific weekday, any other terms, and...
35 | (d=(_|%d)|u=%u){SCHED_DELIMS}{SCHED_TERMS}
36 | (
37 | # Specific daily time, or...
38 | H:M=%H:%M
39 | |
40 | # Hour wildcard or specific hour, any other terms, and specific minute.
41 | H=(_|%H){SCHED_DELIMS}{SCHED_TERMS}M=%M
42 | )
43 | )
44 | ({SCHED_DELIMS}|$)
45 | """
46 |
47 | QUEUE_URL = os.environ["QUEUE_URL"]
48 | QUEUE_MSG_BYTES_MAX = environ_int("QUEUE_MSG_BYTES_MAX")
49 | QUEUE_MSG_FMT_VERSION = "01"
50 |
51 | ARN_DELIM = ":"
52 | BACKUP_ROLE_ARN = os.environ["BACKUP_ROLE_ARN"]
53 | ARN_PARTS = BACKUP_ROLE_ARN.split(ARN_DELIM)
54 | # arn:partition:service:region:account-id:resource-type/resource-id
55 | # [0] [1] [2] [3] [4] [5]
56 | # https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
57 | ARN_PARTS[3] = os.environ.get("AWS_REGION", os.environ["AWS_DEFAULT_REGION"])
58 |
59 |
60 | # 1. Helpers #################################################################
61 |
62 |
63 | def log(entry_type, entry_value, log_level=logging.INFO):
64 | """Take type and value, and emit a JSON-format log entry
65 | """
66 | entry_value_out = json.loads(json.dumps(entry_value, default=str))
67 | # Avoids "Object of type datetime is not JSON serializable" in
68 | # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/9efb462/awslambdaric/lambda_runtime_log_utils.py#L109-L135
69 | #
70 | # The JSON encoder in the AWS Lambda Python runtime isn't configured to
71 | # serialize datatime values in responses returned by AWS's own Python SDK!
72 | #
73 | # Alternative considered:
74 | # https://docs.powertools.aws.dev/lambda/python/latest/core/logger/
75 |
76 | logger.log(
77 | log_level, "", extra={"type": entry_type, "value": entry_value_out}
78 | )
79 |
80 |
81 | def sqs_send_message_log(
82 | cycle_start_str, send_kwargs, result, result_type, log_level
83 | ):
84 | """Log scheduled start (on error), send_message kwargs, and outcome
85 | """
86 | if log_level > logging.INFO:
87 | log("START", cycle_start_str, log_level)
88 | log("KWARGS_SQS_SEND_MESSAGE", send_kwargs, log_level)
89 | log(result_type, result, log_level)
90 |
91 |
92 | def op_log(event, op_msg, result, result_type, log_level):
93 | """Log Lambda event (on error), SQS message (operation), and outcome
94 | """
95 | if log_level > logging.INFO:
96 | log("LAMBDA_EVENT", event, log_level)
97 | log("SQS_MESSAGE", op_msg, log_level)
98 | log(result_type, result, log_level)
99 |
100 |
101 | def assess_op_msg(op_msg):
102 | """Take an operation queue message, return error message, type, retry flag
103 | """
104 | result = None
105 | result_type = ""
106 | retry = True
107 |
108 | if msg_attr_str_decode(op_msg, "version") != QUEUE_MSG_FMT_VERSION:
109 | result = "Unrecognized operation queue message format"
110 | result_type = "WRONG_QUEUE_MSG_FMT"
111 | retry = False
112 |
113 | elif (
114 | int(msg_attr_str_decode(op_msg, "expires"))
115 | < int(datetime.datetime.now(datetime.timezone.utc).timestamp())
116 | ):
117 | result = (
118 | "Schedule fewer operations per 10-minute cycle or "
119 | "increase DoLambdaFnMaximumConcurrency in CloudFormation"
120 | )
121 | result_type = "EXPIRED_OP"
122 | retry = False
123 |
124 | return (result, result_type, retry)
125 |
126 |
127 | def assess_op_except(svc, op_method_name, misc_except):
128 | """Take an operation and an exception, return retry flag and log level
129 |
130 | botocore.exceptions.ClientError is general but statically-defined, making
131 | comparison easier, in a multi-service context, than for service-specific but
132 | dynamically-defined exceptions like
133 | boto3.Client("rds").exceptions.InvalidDBClusterStateFault and
134 | boto3.Client("rds").exceptions.InvalidDBInstanceStateFault
135 |
136 | https://boto3.amazonaws.com/v1/documentation/api/latest/guide/error-handling.html#parsing-error-responses-and-catching-exceptions-from-aws-services
137 | """
138 | retry = True
139 | log_level = logging.ERROR
140 |
141 | if isinstance(misc_except, botocore.exceptions.ClientError):
142 | verb = op_method_name.split("_")[0]
143 | err_dict = getattr(misc_except, "response", {}).get("Error", {})
144 | err_msg = err_dict.get("Message")
145 |
146 | match (svc, err_dict.get("Code")):
147 |
148 | case ("cloudformation", "ValidationError") if (
149 | "No updates are to be performed." == err_msg
150 | ):
151 | retry = False
152 | log_level = logging.INFO
153 | # Idempotent update_stack (after a recent external update)
154 |
155 | case ("rds", "InvalidDBClusterStateFault") if (
156 | ((verb == "start") and "is in available" in err_msg)
157 | or f"is in {verb}" in err_msg
158 | ):
159 | retry = False
160 | log_level = logging.INFO
161 | # Idempotent
162 | # start_db_cluster "in available[ state]" or "in start[ing state]" or
163 | # stop__db_cluster "in stop[ped state]" or "in stop[ping state]"
164 |
165 | case ("rds", "InvalidDBInstanceState"): # Fault suffix is missing here!
166 | retry = False
167 | # Can't decide between idempotent start_db_instance / stop_db_instance
168 | # (common) or truly erroneous state (rare), because message does not
169 | # mention current, invalid state. Log as potential error, but do not
170 | # retry (avoids a duplicate error queue entry).
171 |
172 | return (retry, log_level)
173 |
174 |
175 | def tag_key_join(tag_key_words):
176 | """Take a tuple of strings, add a prefix, join, and return a tag key
177 | """
178 | return "-".join(("sched", ) + tag_key_words)
179 |
180 |
181 | def cycle_start_end(datetime_in, cycle_minutes=10, cutoff_minutes=9):
182 | """Take a datetime, return 10-minute floor and ceiling less 1 minute
183 | """
184 | cycle_minutes_int = int(cycle_minutes)
185 | cycle_start = datetime_in.replace(
186 | minute=(datetime_in.minute // cycle_minutes_int) * cycle_minutes_int,
187 | second=0,
188 | microsecond=0,
189 | )
190 | cycle_cutoff = cycle_start + datetime.timedelta(minutes=cutoff_minutes)
191 | return (cycle_start, cycle_cutoff)
192 |
193 |
194 | def msg_attrs_str_encode(attr_pairs):
195 | """Take list of string name, value pairs, return SQS MessageAttributes dict
196 | """
197 | return {
198 | attr_name: {"DataType": "String", "StringValue": attr_value}
199 | for (attr_name, attr_value) in attr_pairs
200 | }
201 |
202 |
203 | def msg_attr_str_decode(msg, attr_name):
204 | """Take an SQS message, return value of a string attribute (must be present)
205 | """
206 | return msg["messageAttributes"][attr_name]["stringValue"]
207 |
208 |
209 | svc_clients = {}
210 |
211 |
212 | def svc_client_get(svc):
213 | """Take an AWS service, return a boto3 client, creating it if needed
214 |
215 | boto3 method references can only be resolved at run-time, against an
216 | instance of an AWS service's Client class.
217 | http://boto3.readthedocs.io/en/latest/guide/events.html#extensibility-guide
218 |
219 | Alternatives considered:
220 | https://github.com/boto/boto3/issues/3197#issue-1175578228
221 | https://github.com/aws-samples/boto-session-manager-project
222 | """
223 | if svc not in svc_clients:
224 | svc_clients[svc] = boto3.client(
225 | svc, config=botocore.config.Config(retries={"mode": "standard"})
226 | )
227 | return svc_clients[svc]
228 |
229 |
230 | # 2. Custom Classes ##########################################################
231 |
232 | # See rsrc_types_init() for usage examples.
233 |
234 |
235 | class AWSRsrcType(): # pylint: disable=too-many-instance-attributes
236 | """AWS resource type, with identification properties and various operations
237 | """
238 | members = {}
239 |
240 | # pylint: disable=too-many-arguments,too-many-positional-arguments
241 | def __init__(
242 | self,
243 | svc,
244 | rsrc_type_words,
245 | ops_dict,
246 | rsrc_id_key_suffix="Id",
247 | arn_key_suffix="Arn",
248 | tags_key="Tags",
249 | status_filter_pair=()
250 | ):
251 | self.svc = svc
252 | self.name_in_methods = "_".join(rsrc_type_words).lower()
253 |
254 | self.name_in_keys = "".join(rsrc_type_words)
255 | self.rsrc_id_key = f"{self.name_in_keys}{rsrc_id_key_suffix}"
256 | if arn_key_suffix:
257 | self.arn_prefix = ""
258 | self.arn_key = f"{self.name_in_keys}{arn_key_suffix}"
259 | else:
260 | self.arn_prefix = ARN_DELIM.join(
261 | ARN_PARTS[0:2] + [svc] + ARN_PARTS[3:5] + [f"{self.name_in_methods}/"]
262 | )
263 | self.arn_key = self.rsrc_id_key
264 | self.tags_key = tags_key
265 |
266 | self.ops = {}
267 | for (op_tag_key_words, op_properties) in ops_dict.items():
268 | op = AWSOp.new(self, op_tag_key_words, **op_properties)
269 | self.ops[op.tag_key] = op
270 |
271 | self.describe_kwargs = {}
272 | if status_filter_pair:
273 | self.describe_kwargs["Filters"] = [
274 | {"Name": filter_name, "Values": list(filter_values)}
275 | for (filter_name, filter_values)
276 | in [status_filter_pair, ("tag-key", self.ops_tag_keys)]
277 | ]
278 |
279 | self.__class__.members[(svc, self.name_in_keys)] = self # Register me!
280 |
281 | def __str__(self):
282 | return " ".join([self.__class__.__name__, self.svc, self.name_in_keys])
283 |
284 | @property
285 | def ops_tag_keys(self):
286 | """Return tag keys for all operations on this resource type
287 | """
288 | return self.ops.keys()
289 |
290 | def rsrc_id(self, rsrc):
291 | """Take 1 describe_ result, return the resource ID
292 | """
293 | return rsrc[self.rsrc_id_key]
294 |
295 | def arn(self, rsrc):
296 | """Take 1 describe_ result, return the ARN
297 | """
298 | return f"{self.arn_prefix}{rsrc[self.arn_key]}"
299 |
300 | def get_describe_pages(self):
301 | """Return an iterator over pages of boto3 describe_ responses
302 | """
303 | return svc_client_get(self.svc).get_paginator(
304 | f"describe_{self.name_in_methods}s"
305 | ).paginate(**self.describe_kwargs)
306 |
307 | def get_rsrcs(self):
308 | """Return an iterator over individual boto3 describe_ items
309 | """
310 | return (
311 | rsrc
312 | for page in self.get_describe_pages()
313 | for rsrc in page.get(f"{self.name_in_keys}s", [])
314 | )
315 |
316 | def rsrc_tags_list(self, rsrc):
317 | """Take 1 describe_ result, return raw resource tags
318 | """
319 | return rsrc.get(self.tags_key, []) # Key may be missing if no tags
320 |
321 | def op_tags_match(self, rsrc, sched_regexp):
322 | """Scan 1 resource's tags to find operations scheduled for current cycle
323 | """
324 | ops_tag_keys = self.ops_tag_keys
325 | op_tags_matched = []
326 | for tag_dict in self.rsrc_tags_list(rsrc):
327 | tag_key = tag_dict["Key"]
328 | if tag_key in ops_tag_keys and sched_regexp.search(tag_dict["Value"]):
329 | op_tags_matched.append(tag_key)
330 | return op_tags_matched
331 |
332 | def rsrcs_find(self, sched_regexp, cycle_start_str, cycle_cutoff_epoch_str):
333 | """Find resources to operate on, and send details to queue
334 | """
335 | for rsrc in self.get_rsrcs():
336 | op_tags_matched = self.op_tags_match(rsrc, sched_regexp)
337 | op_tags_matched_count = len(op_tags_matched)
338 |
339 | if op_tags_matched_count == 1:
340 | op = self.ops[op_tags_matched[0]]
341 | op.msg_send_to_queue(rsrc, cycle_start_str, cycle_cutoff_epoch_str)
342 | elif op_tags_matched_count > 1:
343 | log("START", cycle_start_str, logging.ERROR)
344 | log(
345 | "MULTIPLE_OPS",
346 | {"arn": self.arn(rsrc), "tag_keys": op_tags_matched},
347 | logging.ERROR
348 | )
349 |
350 |
351 | class AWSRsrcTypeEc2Inst(AWSRsrcType):
352 | """EC2 instance
353 | """
354 | def get_rsrcs(self):
355 | return (
356 | instance
357 | for page in self.get_describe_pages()
358 | for reservation in page.get("Reservations", [])
359 | for instance in reservation.get("Instances", [])
360 | )
361 |
362 |
363 | class AWSOp():
364 | """Operation on 1 AWS resource
365 | """
366 | def __init__(self, rsrc_type, tag_key_words, **kwargs):
367 | self.tag_key = tag_key_join(tag_key_words)
368 | self.svc = rsrc_type.svc
369 | self.rsrc_id = rsrc_type.rsrc_id
370 | verb = kwargs.get("verb", tag_key_words[0]) # Default: 1st word
371 | self.method_name = f"{verb}_{rsrc_type.name_in_methods}"
372 | self.kwarg_rsrc_id_key = rsrc_type.rsrc_id_key
373 | self.kwargs_add = kwargs.get("kwargs_add", {})
374 |
375 | @staticmethod
376 | def new(rsrc_type, tag_key_words, **kwargs):
377 | """Create an op of the requested, appropriate, or default (sub)class
378 | """
379 | if "class" in kwargs:
380 | op_class = kwargs["class"]
381 | elif tag_key_words == ("backup", ):
382 | op_class = AWSOpBackUp
383 | else:
384 | op_class = AWSOp
385 | return op_class(rsrc_type, tag_key_words, **kwargs)
386 |
387 | def kwarg_rsrc_id(self, rsrc):
388 | """Transfer resource ID from a describe_ result to another method's kwarg
389 | """
390 | return {self.kwarg_rsrc_id_key: self.rsrc_id(rsrc)}
391 |
392 | def op_kwargs(self, rsrc, cycle_start_str): # pylint: disable=unused-argument
393 | """Take a describe_ result, return another method's kwargs
394 | """
395 | op_kwargs_out = self.kwarg_rsrc_id(rsrc)
396 | op_kwargs_out.update(self.kwargs_add)
397 | return op_kwargs_out
398 |
399 | def msg_send_to_queue(self, rsrc, cycle_start_str, cycle_cutoff_epoch_str):
400 | """Send 1 operation message to the SQS queue
401 | """
402 | op_kwargs = self.op_kwargs(rsrc, cycle_start_str)
403 | if op_kwargs:
404 | send_kwargs = {
405 | "QueueUrl": QUEUE_URL,
406 | "MessageAttributes": msg_attrs_str_encode((
407 | ("version", QUEUE_MSG_FMT_VERSION),
408 | ("expires", cycle_cutoff_epoch_str),
409 | ("start", cycle_start_str),
410 | ("svc", self.svc),
411 | ("op_method_name", self.method_name),
412 | )),
413 | "MessageBody": op_kwargs,
414 | # Raw only for logging in case of an exception during JSON encoding
415 | }
416 |
417 | result = None
418 | result_type = ""
419 |
420 | log_level = logging.ERROR
421 |
422 | try:
423 | msg_body = json.dumps(op_kwargs)
424 | send_kwargs.update({"MessageBody": msg_body, })
425 |
426 | if QUEUE_MSG_BYTES_MAX < len(bytes(msg_body, "utf-8")):
427 | result = "Increase QueueMessageBytesMax in CloudFormation"
428 | result_type = "QUEUE_MSG_TOO_LONG"
429 |
430 | else:
431 | result = svc_client_get("sqs").send_message(**send_kwargs)
432 | result_type = "AWS_RESPONSE"
433 | log_level = logging.INFO
434 |
435 | except Exception as misc_except: # pylint: disable=broad-exception-caught
436 | result = misc_except
437 | result_type = "EXCEPTION"
438 |
439 | sqs_send_message_log(
440 | cycle_start_str, send_kwargs, result, result_type, log_level
441 | )
442 |
443 | def __str__(self):
444 | return " ".join([
445 | self.__class__.__name__, self.tag_key, self.svc, self.method_name
446 | ])
447 |
448 |
449 | class AWSOpMultipleRsrcs(AWSOp):
450 | """Operation on multiple AWS resources of the same type
451 | """
452 | def __init__(self, rsrc_type, tag_key_words, **kwargs):
453 | super().__init__(rsrc_type, tag_key_words, **kwargs)
454 | self.method_name = self.method_name + "s"
455 |
456 | def kwarg_rsrc_id(self, rsrc):
457 | """Transfer resource ID from a describe_ result to a singleton list kwarg
458 |
459 | One at a time for consistency and to avoid partial completion risk
460 | """
461 | return {f"{self.kwarg_rsrc_id_key}s": [self.rsrc_id(rsrc)]}
462 |
463 |
464 | class AWSOpUpdateStack(AWSOp):
465 | """CloudFormation stack update operation
466 | """
467 | def __init__(self, rsrc_type, tag_key_words, **kwargs):
468 | super().__init__(rsrc_type, tag_key_words, verb="update", **kwargs)
469 |
470 | # Use of final template instead of original makes this incompatible with
471 | # CloudFormation "transforms". describe_stacks does not return templates.
472 | self.kwargs_add.update({
473 | "UsePreviousTemplate": True,
474 | "RetainExceptOnCreate": True,
475 | })
476 |
477 | # Use previous parameter values, except for:
478 | # Param Value
479 | # Key Out
480 | # tag_key sched-set-Enable-true
481 | # tag_key sched-set-Enable-false
482 | # tag_key_words [-2] [-1]
483 | self.changing_param_key = tag_key_words[-2]
484 | self.changing_param_value_out = tag_key_words[-1]
485 |
486 | def op_kwargs(self, rsrc, cycle_start_str):
487 | """Take 1 describe_stacks result, return update_stack kwargs
488 |
489 | An empty dict indicates that no stack update is needed.
490 | """
491 | op_kwargs_out = {}
492 | params_out = []
493 |
494 | if rsrc.get("StackStatus") in (
495 | "UPDATE_COMPLETE",
496 | "CREATE_COMPLETE",
497 | ):
498 | for param in rsrc.get("Parameters", []):
499 | param_key = param["ParameterKey"]
500 | param_out = {
501 | "ParameterKey": param_key,
502 | "UsePreviousValue": True,
503 | }
504 |
505 | if param_key == self.changing_param_key:
506 | if param.get("ParameterValue") == self.changing_param_value_out:
507 | break
508 |
509 | # One time, if changing_param is present and not already up-to-date
510 | param_out.update({
511 | "UsePreviousValue": False,
512 | "ParameterValue": self.changing_param_value_out,
513 | })
514 | op_kwargs_out = super().op_kwargs(rsrc, cycle_start_str)
515 | op_kwargs_out.update({
516 | "ClientRequestToken": f"{self.tag_key}-{cycle_start_str}",
517 | "Parameters": params_out, # Continue updating dict in-place
518 | })
519 | capabilities = rsrc.get("Capabilities")
520 | if capabilities:
521 | op_kwargs_out["Capabilities"] = capabilities
522 |
523 | params_out.append(param_out)
524 |
525 | else:
526 | log(
527 | "STACK_STATUS_IRREGULAR",
528 | "Fix manually until UPDATE_COMPLETE",
529 | logging.ERROR
530 | )
531 | log("AWS_RESPONSE_PART", rsrc, logging.ERROR)
532 |
533 | return op_kwargs_out
534 |
535 |
536 | class AWSOpBackUp(AWSOp):
537 | """On-demand AWS Backup operation
538 | """
539 | backup_kwargs_add = None
540 | lifecycle_base = None
541 |
542 | @classmethod
543 | def backup_kwargs_add_init(cls):
544 | """Populate start_backup_job kwargs and base lifecycle, if not yet done
545 | """
546 | if not cls.backup_kwargs_add:
547 | cls.backup_kwargs_add = {
548 | "IamRoleArn": BACKUP_ROLE_ARN,
549 | "BackupVaultName": os.environ["BACKUP_VAULT_NAME"],
550 | "StartWindowMinutes": environ_int("BACKUP_START_WINDOW_MINUTES"),
551 | "CompleteWindowMinutes": environ_int(
552 | "BACKUP_COMPLETE_WINDOW_MINUTES"
553 | ),
554 | }
555 |
556 | cls.lifecycle_base = {}
557 | cold_storage_after_days = environ_int("BACKUP_COLD_STORAGE_AFTER_DAYS")
558 | if cold_storage_after_days > 0:
559 | cls.lifecycle_base.update({
560 | "OptInToArchiveForSupportedResources": True,
561 | "MoveToColdStorageAfterDays": cold_storage_after_days,
562 | })
563 | delete_after_days = environ_int("BACKUP_DELETE_AFTER_DAYS")
564 | if delete_after_days > 0:
565 | cls.lifecycle_base["DeleteAfterDays"] = delete_after_days # pylint: disable=unsupported-assignment-operation
566 |
567 | def __init__(self, rsrc_type, tag_key_words, **kwargs):
568 | super().__init__(rsrc_type, tag_key_words, **kwargs)
569 | self.rsrc_id = rsrc_type.arn
570 | self.svc = "backup"
571 | self.method_name = "start_backup_job"
572 | self.kwarg_rsrc_id_key = "ResourceArn"
573 | self.__class__.backup_kwargs_add_init()
574 | self.kwargs_add.update(self.__class__.backup_kwargs_add)
575 |
576 | def op_kwargs(self, rsrc, cycle_start_str):
577 | """Take a describe_ result, return start_backup_job kwargs
578 | """
579 | op_kwargs_out = super().op_kwargs(rsrc, cycle_start_str)
580 | op_kwargs_out.update({
581 | "IdempotencyToken": f"{cycle_start_str},{self.rsrc_id(rsrc)}",
582 | # As of May, 2025, AWS Backup treats calls to back up different
583 | # resources as identical so long as IdempotencyToken matches! This is
584 | # contrary to the documentation ("otherwise identical calls"). To assure
585 | # uniqueness, combine scheduled start time, ARN.
586 | # https://docs.aws.amazon.com/aws-backup/latest/devguide/API_StartBackupJob.html#Backup-StartBackupJob-request-IdempotencyToken
587 |
588 | "RecoveryPointTags": {tag_key_join(("time", )): cycle_start_str},
589 | })
590 | lifecycle = dict(self.lifecycle_base) # Updatable copy (future need)
591 | if lifecycle:
592 | op_kwargs_out["Lifecycle"] = lifecycle
593 | return op_kwargs_out
594 |
595 |
596 | # 3. Data-Driven Specifications ##############################################
597 |
598 |
599 | def rsrc_types_init():
600 | """Create AWS resource type objects when needed, if not already done
601 | """
602 |
603 | if not AWSRsrcType.members:
604 | AWSRsrcTypeEc2Inst(
605 | "ec2",
606 | ("Instance", ),
607 | {
608 | ("start", ): {"class": AWSOpMultipleRsrcs},
609 | ("stop", ): {"class": AWSOpMultipleRsrcs},
610 | ("hibernate", ): {
611 | "class": AWSOpMultipleRsrcs,
612 | "verb": "stop",
613 | "kwargs_add": {"Hibernate": True},
614 | },
615 | ("backup", ): {},
616 | },
617 | arn_key_suffix=None,
618 | status_filter_pair=(
619 | "instance-state-name", ("running", "stopping", "stopped")
620 | )
621 | )
622 |
623 | AWSRsrcType(
624 | "ec2",
625 | ("Volume", ),
626 | {("backup", ): {}},
627 | arn_key_suffix=None,
628 | status_filter_pair=("status", ("available", "in-use")),
629 | )
630 |
631 | AWSRsrcType(
632 | "rds",
633 | ("DB", "Instance"),
634 | {
635 | ("start", ): {},
636 | ("stop", ): {},
637 | ("backup", ): {},
638 | },
639 | rsrc_id_key_suffix="Identifier",
640 | tags_key="TagList",
641 | )
642 |
643 | AWSRsrcType(
644 | "rds",
645 | ("DB", "Cluster"),
646 | {
647 | ("start", ): {},
648 | ("stop", ): {},
649 | ("backup", ): {},
650 | },
651 | rsrc_id_key_suffix="Identifier",
652 | tags_key="TagList",
653 | )
654 |
655 | if "ENABLE_SCHED_CLOUDFORMATION_OPS" in os.environ:
656 | AWSRsrcType(
657 | "cloudformation",
658 | ("Stack", ),
659 | {
660 | ("set", "Enable", "true"): {"class": AWSOpUpdateStack},
661 | ("set", "Enable", "false"): {"class": AWSOpUpdateStack},
662 | },
663 | rsrc_id_key_suffix="Name",
664 | arn_key_suffix="Id",
665 | )
666 |
667 |
668 | # 4. Find Resources Lambda Function Handler ##################################
669 |
670 |
671 | def lambda_handler_find(event, context): # pylint: disable=unused-argument
672 | """Find and queue AWS resources for scheduled operations, based on tags
673 | """
674 | log("LAMBDA_EVENT", event)
675 | (cycle_start, cycle_cutoff) = cycle_start_end(
676 | datetime.datetime.now(datetime.timezone.utc)
677 | )
678 |
679 | # ISO 8601 basic, no puctuation (downstream requirement)
680 | cycle_start_str = cycle_start.strftime("%Y%m%dT%H%MZ")
681 |
682 | cycle_cutoff_epoch_str = str(int(cycle_cutoff.timestamp()))
683 | sched_regexp = re.compile(
684 | cycle_start.strftime(SCHED_REGEXP_STRFTIME_FMT), re.VERBOSE
685 | )
686 | log("START", cycle_start_str)
687 | log("SCHED_REGEXP_VERBOSE", sched_regexp.pattern, logging.DEBUG)
688 | rsrc_types_init()
689 | for rsrc_type in AWSRsrcType.members.values():
690 | try:
691 | rsrc_type.rsrcs_find(
692 | sched_regexp, cycle_start_str, cycle_cutoff_epoch_str
693 | )
694 | except Exception as misc_except: # pylint: disable=broad-exception-caught
695 | log("EXCEPTION", misc_except, logging.ERROR)
696 | # Allow continue to next describe_ call (likely that permissions differ)
697 |
698 | # 5. "Do" Operations Lambda Function Handler #################################
699 |
700 |
701 | def lambda_handler_do(event, context): # pylint: disable=unused-argument
702 | """Perform a queued operation on an AWS resource
703 | """
704 | batch_item_failures = []
705 |
706 | for op_msg in event.get("Records", []):
707 | sqs_msg_id = ""
708 |
709 | result = None
710 | result_type = ""
711 | retry = True
712 |
713 | log_level = logging.ERROR
714 |
715 | try:
716 | sqs_msg_id = op_msg["messageId"]
717 | (result, result_type, retry) = assess_op_msg(op_msg)
718 | if not result_type:
719 | svc = msg_attr_str_decode(op_msg, "svc")
720 | op_method_name = msg_attr_str_decode(op_msg, "op_method_name")
721 | op_kwargs = json.loads(op_msg["body"])
722 | op_method = getattr(svc_client_get(svc), op_method_name)
723 | result = op_method(**op_kwargs)
724 | result_type = "AWS_RESPONSE"
725 | retry = False
726 | log_level = logging.INFO
727 |
728 | except Exception as misc_except: # pylint: disable=broad-exception-caught
729 | result = misc_except
730 | result_type = "EXCEPTION"
731 | (retry, log_level) = assess_op_except(svc, op_method_name, misc_except)
732 |
733 | op_log(event, op_msg, result, result_type, log_level)
734 |
735 | if retry and sqs_msg_id:
736 | batch_item_failures.append({"itemIdentifier": sqs_msg_id, })
737 |
738 | # https://repost.aws/knowledge-center/lambda-sqs-report-batch-item-failures
739 | return {"batchItemFailures": batch_item_failures, }
740 |
--------------------------------------------------------------------------------
/media/lights-off-aws-architecture-and-flow-thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sqlxpert/lights-off-aws/024e4eb448b9c0595316a4a0d6f48032ec6bcfb1/media/lights-off-aws-architecture-and-flow-thumb.png
--------------------------------------------------------------------------------
/media/lights-off-aws-architecture-and-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sqlxpert/lights-off-aws/024e4eb448b9c0595316a4a0d6f48032ec6bcfb1/media/lights-off-aws-architecture-and-flow.png
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | # Start, stop and back up AWS resources tagged with cron schedules
2 | # github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
3 |
4 | [MAIN]
5 |
6 | load-plugins=pylint.extensions.no_self_use
7 |
8 | [MESSAGES CONTROL]
9 |
10 | disable=fixme,locally-disabled
11 |
12 | [REPORTS]
13 |
14 | reports=no
15 |
16 | [VARIABLES]
17 |
18 | max-locals=25
19 |
20 | [BASIC]
21 |
22 | argument-rgx=[a-z_][a-z0-9_]{1,30}$
23 | variable-rgx=[a-z_][a-z0-9_]{1,30}$
24 |
25 | [FORMAT]
26 |
27 | indent-string=" "
28 | indent-after-paren=2
29 | max-line-length=80
30 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Start, stop and back up AWS resources tagged with cron schedules
2 | # github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
3 |
4 | boto3
5 | botocore
6 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | # Start, stop and back up AWS resources tagged with cron schedules
2 | # github.com/sqlxpert/lights-off-aws/ GPLv3 Copyright Paul Marcelin
3 |
4 | [pycodestyle]
5 | ignore = E111, E121, E122, E131, E114, E251, E501, W503
6 | # E501: Let pylint, which allows temporary, inline disabling, check line length
7 | # max-line-length = 80
8 | # E122, E251: These checks interfere with readable formatting of blocks of
9 | # argparse option help text
10 | # E131 forces hanging indents
11 |
--------------------------------------------------------------------------------