Merge ~dmzoneill/charm-advanced-routing:dev/upgrade into ~dmzoneill/charm-advanced-routing:master
- Git
- lp:~dmzoneill/charm-advanced-routing
- dev/upgrade
- Merge into master
Status: | Needs review |
---|---|
Proposed branch: | ~dmzoneill/charm-advanced-routing:dev/upgrade |
Merge into: | ~dmzoneill/charm-advanced-routing:master |
Diff against target: |
1956 lines (+1785/-0) 28 files modified
.gitignore (+12/-0) LICENSE (+202/-0) Makefile (+49/-0) README (+0/-0) README.md (+49/-0) config.yaml (+17/-0) example_config.yaml (+27/-0) icon.svg (+279/-0) interfaces/.empty (+1/-0) layer.yaml (+2/-0) layers/.empty (+1/-0) lib/AdvancedRoutingHelper.py (+132/-0) lib/RoutingEntry.py (+221/-0) lib/RoutingValidator.py (+182/-0) metadata.yaml (+16/-0) reactive/advanced_routing.py (+56/-0) requirements.txt (+3/-0) revision (+1/-0) tests/bundles/bionic.yaml (+45/-0) tests/bundles/overlays/bionic.yaml.j2 (+2/-0) tests/functional/conftest.py (+71/-0) tests/functional/juju_tools.py (+69/-0) tests/functional/requirements.txt (+6/-0) tests/functional/test_routing.py (+54/-0) tests/unit/conftest.py (+80/-0) tests/unit/requirements.txt (+8/-0) tests/unit/test_AdvancedRoutingHelper.py (+154/-0) tox.ini (+46/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeremy Lounder (community) | Approve | ||
Drew Freiberger (community) | Needs Fixing | ||
David O Neill | Pending | ||
Review via email: mp+371884@code.launchpad.net |
Commit message
Upgraded to reactive and added static routing
- Upgraded charm to reactive
- Cleaned up old libraries
- Add parser, extensibility to different routing, table, rule configuration options
- Change to json blob in juju config item
- Added multiple sanity checks and verification + better juju logging
- Added functionality testing with libjuju
expected config: based on example_config.yaml
root@
100 SF1
root@
# This file is managed by Juju.
sudo ip rule del from 192.170.2.0/24 to 192.170.2.0/24 lookup SF1
sudo ip route del 6.6.6.0/24 via 10.191.86.2
sudo ip route del default via 10.191.86.2 table SF1 dev eth0
sudo ip route flush table SF1
sudo ip rule del table SF1
sudo ip route flush cache
root@
# This file is managed by Juju.
sudo ip route flush cache
# Table: name SF1
sudo ip route replace default via 10.191.86.2 table SF1 dev eth0
sudo ip route replace 6.6.6.0/24 via 10.191.86.2
sudo ip rule add from 192.170.2.0/24 to 192.170.2.0/24 lookup SF1
Description of the change
- 25b0cd4... by David O Neill
-
Fixes and improvements from review 371884 MR
* replaced format for all string formatting
* replaced exits with raise exception
* solved a bug of duplicate entries in ip/down script
* numerous of code tidy ups and best practices
* linted it
* updates for unit and functional testing
Drew Freiberger (afreiberger) wrote : | # |
Now that you're raising excceptions, you can catch them in the reactive state machine and set statuses based off of a try: setup: except: set_state(blocked, blocked due to error "foo" in setup
Jeremy Lounder (jldev) wrote : | # |
This merge was already completed via an intermediary branch.
Paul Goins (vultaire) wrote : | # |
The MR here is for merging into David's branch, however these commits are the exact same (hashes and all) as the first 2 commits in the central repo.
Unmerged commits
- 25b0cd4... by David O Neill
-
Fixes and improvements from review 371884 MR
* replaced format for all string formatting
* replaced exits with raise exception
* solved a bug of duplicate entries in ip/down script
* numerous of code tidy ups and best practices
* linted it
* updates for unit and functional testing - 2a2c57d... by David O Neill
-
Migrate from hooks to reactive based charm
Upgraded to reactive and added static routing
- Upgraded charm to reactive
- Cleaned up old libraries
- Add parser, extensibility to different routing, table, rule configuration options
- Change to json blob in juju config item
- Added multiple sanity checks and verification + better juju logging
- Added functionality testing with libjujuexpected config: based on example_config.yaml
root@
juju-720cde- 24:/home/ ubuntu# cat /etc/iproute2/ rt_tables. d/juju- managed. conf
100 SF1root@
juju-720cde- 24:/home/ ubuntu# cat /usr/local/ sbin/if- down/95- juju_routing
# This file is managed by Juju.
sudo ip rule del from 192.170.2.0/24 to 192.170.2.0/24 lookup SF1
sudo ip route del 6.6.6.0/24 via 10.191.86.2
sudo ip route del default via 10.191.86.2 table SF1 dev eth0
sudo ip route flush table SF1
sudo ip rule del table SF1
sudo ip route flush cacheroot@
juju-720cde- 24:/home/ ubuntu# cat /usr/local/ sbin/if- up/95-juju_ routing
# This file is managed by Juju.
sudo ip route flush cache
# Table: name SF1
sudo ip route replace default via 10.191.86.2 table SF1 dev eth0
sudo ip route replace 6.6.6.0/24 via 10.191.86.2
sudo ip rule add from 192.170.2.0/24 to 192.170.2.0/24 lookup SF1Co-authored-by: Diko Parvanov <email address hidden>
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 0000000..ebec07a |
4 | --- /dev/null |
5 | +++ b/.gitignore |
6 | @@ -0,0 +1,12 @@ |
7 | +*.swp |
8 | +*~ |
9 | +.idea |
10 | +.tox |
11 | +.vscode |
12 | +/builds/ |
13 | +/deb_dist |
14 | +/dist |
15 | +/repo-info |
16 | +__pycache__ |
17 | + |
18 | + |
19 | diff --git a/LICENSE b/LICENSE |
20 | new file mode 100644 |
21 | index 0000000..d645695 |
22 | --- /dev/null |
23 | +++ b/LICENSE |
24 | @@ -0,0 +1,202 @@ |
25 | + |
26 | + Apache License |
27 | + Version 2.0, January 2004 |
28 | + http://www.apache.org/licenses/ |
29 | + |
30 | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
31 | + |
32 | + 1. Definitions. |
33 | + |
34 | + "License" shall mean the terms and conditions for use, reproduction, |
35 | + and distribution as defined by Sections 1 through 9 of this document. |
36 | + |
37 | + "Licensor" shall mean the copyright owner or entity authorized by |
38 | + the copyright owner that is granting the License. |
39 | + |
40 | + "Legal Entity" shall mean the union of the acting entity and all |
41 | + other entities that control, are controlled by, or are under common |
42 | + control with that entity. For the purposes of this definition, |
43 | + "control" means (i) the power, direct or indirect, to cause the |
44 | + direction or management of such entity, whether by contract or |
45 | + otherwise, or (ii) ownership of fifty percent (50%) or more of the |
46 | + outstanding shares, or (iii) beneficial ownership of such entity. |
47 | + |
48 | + "You" (or "Your") shall mean an individual or Legal Entity |
49 | + exercising permissions granted by this License. |
50 | + |
51 | + "Source" form shall mean the preferred form for making modifications, |
52 | + including but not limited to software source code, documentation |
53 | + source, and configuration files. |
54 | + |
55 | + "Object" form shall mean any form resulting from mechanical |
56 | + transformation or translation of a Source form, including but |
57 | + not limited to compiled object code, generated documentation, |
58 | + and conversions to other media types. |
59 | + |
60 | + "Work" shall mean the work of authorship, whether in Source or |
61 | + Object form, made available under the License, as indicated by a |
62 | + copyright notice that is included in or attached to the work |
63 | + (an example is provided in the Appendix below). |
64 | + |
65 | + "Derivative Works" shall mean any work, whether in Source or Object |
66 | + form, that is based on (or derived from) the Work and for which the |
67 | + editorial revisions, annotations, elaborations, or other modifications |
68 | + represent, as a whole, an original work of authorship. For the purposes |
69 | + of this License, Derivative Works shall not include works that remain |
70 | + separable from, or merely link (or bind by name) to the interfaces of, |
71 | + the Work and Derivative Works thereof. |
72 | + |
73 | + "Contribution" shall mean any work of authorship, including |
74 | + the original version of the Work and any modifications or additions |
75 | + to that Work or Derivative Works thereof, that is intentionally |
76 | + submitted to Licensor for inclusion in the Work by the copyright owner |
77 | + or by an individual or Legal Entity authorized to submit on behalf of |
78 | + the copyright owner. For the purposes of this definition, "submitted" |
79 | + means any form of electronic, verbal, or written communication sent |
80 | + to the Licensor or its representatives, including but not limited to |
81 | + communication on electronic mailing lists, source code control systems, |
82 | + and issue tracking systems that are managed by, or on behalf of, the |
83 | + Licensor for the purpose of discussing and improving the Work, but |
84 | + excluding communication that is conspicuously marked or otherwise |
85 | + designated in writing by the copyright owner as "Not a Contribution." |
86 | + |
87 | + "Contributor" shall mean Licensor and any individual or Legal Entity |
88 | + on behalf of whom a Contribution has been received by Licensor and |
89 | + subsequently incorporated within the Work. |
90 | + |
91 | + 2. Grant of Copyright License. Subject to the terms and conditions of |
92 | + this License, each Contributor hereby grants to You a perpetual, |
93 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
94 | + copyright license to reproduce, prepare Derivative Works of, |
95 | + publicly display, publicly perform, sublicense, and distribute the |
96 | + Work and such Derivative Works in Source or Object form. |
97 | + |
98 | + 3. Grant of Patent License. Subject to the terms and conditions of |
99 | + this License, each Contributor hereby grants to You a perpetual, |
100 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
101 | + (except as stated in this section) patent license to make, have made, |
102 | + use, offer to sell, sell, import, and otherwise transfer the Work, |
103 | + where such license applies only to those patent claims licensable |
104 | + by such Contributor that are necessarily infringed by their |
105 | + Contribution(s) alone or by combination of their Contribution(s) |
106 | + with the Work to which such Contribution(s) was submitted. If You |
107 | + institute patent litigation against any entity (including a |
108 | + cross-claim or counterclaim in a lawsuit) alleging that the Work |
109 | + or a Contribution incorporated within the Work constitutes direct |
110 | + or contributory patent infringement, then any patent licenses |
111 | + granted to You under this License for that Work shall terminate |
112 | + as of the date such litigation is filed. |
113 | + |
114 | + 4. Redistribution. You may reproduce and distribute copies of the |
115 | + Work or Derivative Works thereof in any medium, with or without |
116 | + modifications, and in Source or Object form, provided that You |
117 | + meet the following conditions: |
118 | + |
119 | + (a) You must give any other recipients of the Work or |
120 | + Derivative Works a copy of this License; and |
121 | + |
122 | + (b) You must cause any modified files to carry prominent notices |
123 | + stating that You changed the files; and |
124 | + |
125 | + (c) You must retain, in the Source form of any Derivative Works |
126 | + that You distribute, all copyright, patent, trademark, and |
127 | + attribution notices from the Source form of the Work, |
128 | + excluding those notices that do not pertain to any part of |
129 | + the Derivative Works; and |
130 | + |
131 | + (d) If the Work includes a "NOTICE" text file as part of its |
132 | + distribution, then any Derivative Works that You distribute must |
133 | + include a readable copy of the attribution notices contained |
134 | + within such NOTICE file, excluding those notices that do not |
135 | + pertain to any part of the Derivative Works, in at least one |
136 | + of the following places: within a NOTICE text file distributed |
137 | + as part of the Derivative Works; within the Source form or |
138 | + documentation, if provided along with the Derivative Works; or, |
139 | + within a display generated by the Derivative Works, if and |
140 | + wherever such third-party notices normally appear. The contents |
141 | + of the NOTICE file are for informational purposes only and |
142 | + do not modify the License. You may add Your own attribution |
143 | + notices within Derivative Works that You distribute, alongside |
144 | + or as an addendum to the NOTICE text from the Work, provided |
145 | + that such additional attribution notices cannot be construed |
146 | + as modifying the License. |
147 | + |
148 | + You may add Your own copyright statement to Your modifications and |
149 | + may provide additional or different license terms and conditions |
150 | + for use, reproduction, or distribution of Your modifications, or |
151 | + for any such Derivative Works as a whole, provided Your use, |
152 | + reproduction, and distribution of the Work otherwise complies with |
153 | + the conditions stated in this License. |
154 | + |
155 | + 5. Submission of Contributions. Unless You explicitly state otherwise, |
156 | + any Contribution intentionally submitted for inclusion in the Work |
157 | + by You to the Licensor shall be under the terms and conditions of |
158 | + this License, without any additional terms or conditions. |
159 | + Notwithstanding the above, nothing herein shall supersede or modify |
160 | + the terms of any separate license agreement you may have executed |
161 | + with Licensor regarding such Contributions. |
162 | + |
163 | + 6. Trademarks. This License does not grant permission to use the trade |
164 | + names, trademarks, service marks, or product names of the Licensor, |
165 | + except as required for reasonable and customary use in describing the |
166 | + origin of the Work and reproducing the content of the NOTICE file. |
167 | + |
168 | + 7. Disclaimer of Warranty. Unless required by applicable law or |
169 | + agreed to in writing, Licensor provides the Work (and each |
170 | + Contributor provides its Contributions) on an "AS IS" BASIS, |
171 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
172 | + implied, including, without limitation, any warranties or conditions |
173 | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
174 | + PARTICULAR PURPOSE. You are solely responsible for determining the |
175 | + appropriateness of using or redistributing the Work and assume any |
176 | + risks associated with Your exercise of permissions under this License. |
177 | + |
178 | + 8. Limitation of Liability. In no event and under no legal theory, |
179 | + whether in tort (including negligence), contract, or otherwise, |
180 | + unless required by applicable law (such as deliberate and grossly |
181 | + negligent acts) or agreed to in writing, shall any Contributor be |
182 | + liable to You for damages, including any direct, indirect, special, |
183 | + incidental, or consequential damages of any character arising as a |
184 | + result of this License or out of the use or inability to use the |
185 | + Work (including but not limited to damages for loss of goodwill, |
186 | + work stoppage, computer failure or malfunction, or any and all |
187 | + other commercial damages or losses), even if such Contributor |
188 | + has been advised of the possibility of such damages. |
189 | + |
190 | + 9. Accepting Warranty or Additional Liability. While redistributing |
191 | + the Work or Derivative Works thereof, You may choose to offer, |
192 | + and charge a fee for, acceptance of support, warranty, indemnity, |
193 | + or other liability obligations and/or rights consistent with this |
194 | + License. However, in accepting such obligations, You may act only |
195 | + on Your own behalf and on Your sole responsibility, not on behalf |
196 | + of any other Contributor, and only if You agree to indemnify, |
197 | + defend, and hold each Contributor harmless for any liability |
198 | + incurred by, or claims asserted against, such Contributor by reason |
199 | + of your accepting any such warranty or additional liability. |
200 | + |
201 | + END OF TERMS AND CONDITIONS |
202 | + |
203 | + APPENDIX: How to apply the Apache License to your work. |
204 | + |
205 | + To apply the Apache License to your work, attach the following |
206 | + boilerplate notice, with the fields enclosed by brackets "[]" |
207 | + replaced with your own identifying information. (Don't include |
208 | + the brackets!) The text should be enclosed in the appropriate |
209 | + comment syntax for the file format. We also recommend that a |
210 | + file or class name and description of purpose be included on the |
211 | + same "printed page" as the copyright notice for easier |
212 | + identification within third-party archives. |
213 | + |
214 | + Copyright [yyyy] [name of copyright owner] |
215 | + |
216 | + Licensed under the Apache License, Version 2.0 (the "License"); |
217 | + you may not use this file except in compliance with the License. |
218 | + You may obtain a copy of the License at |
219 | + |
220 | + http://www.apache.org/licenses/LICENSE-2.0 |
221 | + |
222 | + Unless required by applicable law or agreed to in writing, software |
223 | + distributed under the License is distributed on an "AS IS" BASIS, |
224 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
225 | + See the License for the specific language governing permissions and |
226 | + limitations under the License. |
227 | diff --git a/Makefile b/Makefile |
228 | new file mode 100644 |
229 | index 0000000..a8c1fd7 |
230 | --- /dev/null |
231 | +++ b/Makefile |
232 | @@ -0,0 +1,49 @@ |
233 | +help: |
234 | + @echo "This project supports the following targets" |
235 | + @echo "" |
236 | + @echo " make help - show this text" |
237 | + @echo " make submodules - make sure that the submodules are up-to-date" |
238 | + @echo " make lint - run flake8" |
239 | + @echo " make test - run the unittests and lint" |
240 | + @echo " make unittest - run the tests defined in the unittest subdirectory" |
241 | + @echo " make functional - run the tests defined in the functional subdirectory" |
242 | + @echo " make release - build the charm" |
243 | + @echo " make clean - remove unneeded files" |
244 | + @echo "" |
245 | + |
246 | +submodules: |
247 | + @echo "Cloning submodules" |
248 | + @git submodule update --init --recursive |
249 | + |
250 | +lint: |
251 | + @echo "Running flake8" |
252 | + @tox -e lint |
253 | + |
254 | +test: unittest functional lint |
255 | + |
256 | +unittest: |
257 | + @tox -e unit |
258 | + |
259 | +functional: build |
260 | + @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \ |
261 | + PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \ |
262 | + PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \ |
263 | + tox -e functional |
264 | + |
265 | +build: |
266 | + @echo "Building charm to base directory $(JUJU_REPOSITORY)" |
267 | + @-git describe --tags > ./repo-info |
268 | + @LAYER_PATH=./layers INTERFACE_PATH=./interfaces TERM=linux \ |
269 | + JUJU_REPOSITORY=$(JUJU_REPOSITORY) charm build . --force |
270 | + |
271 | +release: clean build |
272 | + @echo "Charm is built at $(JUJU_REPOSITORY)/builds" |
273 | + |
274 | +clean: |
275 | + @echo "Cleaning files" |
276 | + @if [ -d .tox ] ; then rm -r .tox ; fi |
277 | + @if [ -d .pytest_cache ] ; then rm -r .pytest_cache ; fi |
278 | + @find . -iname __pycache__ -exec rm -r {} + |
279 | + |
280 | +# The targets below don't depend on a file |
281 | +.PHONY: lint test unittest functional build release clean help submodules |
282 | diff --git a/README b/README |
283 | new file mode 100644 |
284 | index 0000000..e69de29 |
285 | --- /dev/null |
286 | +++ b/README |
287 | diff --git a/README.md b/README.md |
288 | new file mode 100644 |
289 | index 0000000..f03175f |
290 | --- /dev/null |
291 | +++ b/README.md |
292 | @@ -0,0 +1,49 @@ |
293 | +# Overview |
294 | + |
295 | +This subordinate charm allows for the configuration of simple policy routing rules on the deployed host |
296 | +and adding routing to configured services via a JSON. |
297 | + |
298 | +# Usage |
299 | + |
300 | + |
301 | +# Build |
302 | +``` |
303 | +cd charm-advanced-routing |
304 | +make build |
305 | +``` |
306 | + |
307 | +# Usage |
308 | +Add to an existing application using juju-info relation. |
309 | + |
310 | +Example: |
311 | +``` |
312 | +juju deploy cs:~canonical-bootstack/routing |
313 | +juju add-relation ubuntu advanced-routing |
314 | +``` |
315 | + |
316 | +# Configuration |
317 | +The user can configure the following parameters: |
318 | +* enable-advanced-routing: Enable routing. This requires for the charm to have JSON with routing information configured: ```juju config advanced-routing --file path/to/your/config``` |
319 | + |
320 | +A example_config.json file is provided with the codebase. |
321 | + |
322 | +# Testing |
323 | +To run lint tests: |
324 | +```bash |
325 | +tox -e lint |
326 | + |
327 | +``` |
328 | +To run unit tests: |
329 | +```bash |
330 | +tox -e unit |
331 | +``` |
332 | +Functional tests have been developed using python-libjuju, deploying a simple ubuntu charm and adding the charm as a subordinate. |
333 | + |
334 | +To run tests using python-libjuju: |
335 | +```bash |
336 | +tox -e functional |
337 | +``` |
338 | + |
339 | +# Contact Information |
340 | +David O Neill <david.o.neill@canonical.com> |
341 | + |
342 | diff --git a/config.yaml b/config.yaml |
343 | new file mode 100644 |
344 | index 0000000..eb6c6de |
345 | --- /dev/null |
346 | +++ b/config.yaml |
347 | @@ -0,0 +1,17 @@ |
348 | +options: |
349 | + enable-advanced-routing: |
350 | + type: boolean |
351 | + default: False |
352 | + description: | |
353 | + Wheter to use the custom routing configuration provided by the charm. |
354 | + advanced-routing-config: |
355 | + type: string |
356 | + default: "" |
357 | + description: | |
358 | + A json array consisting of objects, e.g. |
359 | + [ { |
360 | + "type": "route", |
361 | + "net": "192.170.1.0/24", |
362 | + "device": "eth5" |
363 | + }, { ... } ] |
364 | + @ see exmaple_config.yaml for more examples |
365 | \ No newline at end of file |
366 | diff --git a/example_config.yaml b/example_config.yaml |
367 | new file mode 100644 |
368 | index 0000000..6785806 |
369 | --- /dev/null |
370 | +++ b/example_config.yaml |
371 | @@ -0,0 +1,27 @@ |
372 | +settings: |
373 | + advanced-routing-config: |
374 | + value: |- |
375 | + [ { |
376 | + "type": "table", |
377 | + "table": "SF1" |
378 | + }, { |
379 | + "type": "route", |
380 | + "default_route": true, |
381 | + "net": "192.170.1.0/24", |
382 | + "gateway": "10.191.86.2", |
383 | + "table": "SF1", |
384 | + "metric": 101, |
385 | + "device": "eth0" |
386 | + }, { |
387 | + "type": "route", |
388 | + "net": "6.6.6.0/24", |
389 | + "gateway": "10.191.86.2" |
390 | + }, { |
391 | + "type": "rule", |
392 | + "from-net": "192.170.2.0/24", |
393 | + "to-net": "192.170.2.0/24", |
394 | + "table": "SF1", |
395 | + "priority": 101 |
396 | + } ] |
397 | + enable-advanced-routing: |
398 | + value: true |
399 | diff --git a/icon.svg b/icon.svg |
400 | new file mode 100644 |
401 | index 0000000..e092eef |
402 | --- /dev/null |
403 | +++ b/icon.svg |
404 | @@ -0,0 +1,279 @@ |
405 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
406 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> |
407 | + |
408 | +<svg |
409 | + xmlns:dc="http://purl.org/dc/elements/1.1/" |
410 | + xmlns:cc="http://creativecommons.org/ns#" |
411 | + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
412 | + xmlns:svg="http://www.w3.org/2000/svg" |
413 | + xmlns="http://www.w3.org/2000/svg" |
414 | + xmlns:xlink="http://www.w3.org/1999/xlink" |
415 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
416 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
417 | + width="96" |
418 | + height="96" |
419 | + id="svg6517" |
420 | + version="1.1" |
421 | + inkscape:version="0.48+devel r12274" |
422 | + sodipodi:docname="Juju_charm_icon_template.svg"> |
423 | + <defs |
424 | + id="defs6519"> |
425 | + <linearGradient |
426 | + inkscape:collect="always" |
427 | + xlink:href="#Background" |
428 | + id="linearGradient6461" |
429 | + gradientUnits="userSpaceOnUse" |
430 | + x1="0" |
431 | + y1="970.29498" |
432 | + x2="144" |
433 | + y2="970.29498" |
434 | + gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" /> |
435 | + <linearGradient |
436 | + id="Background"> |
437 | + <stop |
438 | + id="stop4178" |
439 | + offset="0" |
440 | + style="stop-color:#b8b8b8;stop-opacity:1" /> |
441 | + <stop |
442 | + id="stop4180" |
443 | + offset="1" |
444 | + style="stop-color:#c9c9c9;stop-opacity:1" /> |
445 | + </linearGradient> |
446 | + <filter |
447 | + style="color-interpolation-filters:sRGB;" |
448 | + inkscape:label="Inner Shadow" |
449 | + id="filter1121"> |
450 | + <feFlood |
451 | + flood-opacity="0.59999999999999998" |
452 | + flood-color="rgb(0,0,0)" |
453 | + result="flood" |
454 | + id="feFlood1123" /> |
455 | + <feComposite |
456 | + in="flood" |
457 | + in2="SourceGraphic" |
458 | + operator="out" |
459 | + result="composite1" |
460 | + id="feComposite1125" /> |
461 | + <feGaussianBlur |
462 | + in="composite1" |
463 | + stdDeviation="1" |
464 | + result="blur" |
465 | + id="feGaussianBlur1127" /> |
466 | + <feOffset |
467 | + dx="0" |
468 | + dy="2" |
469 | + result="offset" |
470 | + id="feOffset1129" /> |
471 | + <feComposite |
472 | + in="offset" |
473 | + in2="SourceGraphic" |
474 | + operator="atop" |
475 | + result="composite2" |
476 | + id="feComposite1131" /> |
477 | + </filter> |
478 | + <filter |
479 | + style="color-interpolation-filters:sRGB;" |
480 | + inkscape:label="Drop Shadow" |
481 | + id="filter950"> |
482 | + <feFlood |
483 | + flood-opacity="0.25" |
484 | + flood-color="rgb(0,0,0)" |
485 | + result="flood" |
486 | + id="feFlood952" /> |
487 | + <feComposite |
488 | + in="flood" |
489 | + in2="SourceGraphic" |
490 | + operator="in" |
491 | + result="composite1" |
492 | + id="feComposite954" /> |
493 | + <feGaussianBlur |
494 | + in="composite1" |
495 | + stdDeviation="1" |
496 | + result="blur" |
497 | + id="feGaussianBlur956" /> |
498 | + <feOffset |
499 | + dx="0" |
500 | + dy="1" |
501 | + result="offset" |
502 | + id="feOffset958" /> |
503 | + <feComposite |
504 | + in="SourceGraphic" |
505 | + in2="offset" |
506 | + operator="over" |
507 | + result="composite2" |
508 | + id="feComposite960" /> |
509 | + </filter> |
510 | + <clipPath |
511 | + clipPathUnits="userSpaceOnUse" |
512 | + id="clipPath873"> |
513 | + <g |
514 | + transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)" |
515 | + id="g875" |
516 | + inkscape:label="Layer 1" |
517 | + style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"> |
518 | + <path |
519 | + style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline" |
520 | + d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z" |
521 | + id="path877" |
522 | + inkscape:connector-curvature="0" |
523 | + sodipodi:nodetypes="sssssssss" /> |
524 | + </g> |
525 | + </clipPath> |
526 | + <filter |
527 | + inkscape:collect="always" |
528 | + id="filter891" |
529 | + inkscape:label="Badge Shadow"> |
530 | + <feGaussianBlur |
531 | + inkscape:collect="always" |
532 | + stdDeviation="0.71999962" |
533 | + id="feGaussianBlur893" /> |
534 | + </filter> |
535 | + </defs> |
536 | + <sodipodi:namedview |
537 | + id="base" |
538 | + pagecolor="#ffffff" |
539 | + bordercolor="#666666" |
540 | + borderopacity="1.0" |
541 | + inkscape:pageopacity="0.0" |
542 | + inkscape:pageshadow="2" |
543 | + inkscape:zoom="4.0745362" |
544 | + inkscape:cx="18.514671" |
545 | + inkscape:cy="49.018169" |
546 | + inkscape:document-units="px" |
547 | + inkscape:current-layer="layer1" |
548 | + showgrid="true" |
549 | + fit-margin-top="0" |
550 | + fit-margin-left="0" |
551 | + fit-margin-right="0" |
552 | + fit-margin-bottom="0" |
553 | + inkscape:window-width="1920" |
554 | + inkscape:window-height="1029" |
555 | + inkscape:window-x="0" |
556 | + inkscape:window-y="24" |
557 | + inkscape:window-maximized="1" |
558 | + showborder="true" |
559 | + showguides="true" |
560 | + inkscape:guide-bbox="true" |
561 | + inkscape:showpageshadow="false"> |
562 | + <inkscape:grid |
563 | + type="xygrid" |
564 | + id="grid821" /> |
565 | + <sodipodi:guide |
566 | + orientation="1,0" |
567 | + position="16,48" |
568 | + id="guide823" /> |
569 | + <sodipodi:guide |
570 | + orientation="0,1" |
571 | + position="64,80" |
572 | + id="guide825" /> |
573 | + <sodipodi:guide |
574 | + orientation="1,0" |
575 | + position="80,40" |
576 | + id="guide827" /> |
577 | + <sodipodi:guide |
578 | + orientation="0,1" |
579 | + position="64,16" |
580 | + id="guide829" /> |
581 | + </sodipodi:namedview> |
582 | + <metadata |
583 | + id="metadata6522"> |
584 | + <rdf:RDF> |
585 | + <cc:Work |
586 | + rdf:about=""> |
587 | + <dc:format>image/svg+xml</dc:format> |
588 | + <dc:type |
589 | + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
590 | + <dc:title></dc:title> |
591 | + </cc:Work> |
592 | + </rdf:RDF> |
593 | + </metadata> |
594 | + <g |
595 | + inkscape:label="BACKGROUND" |
596 | + inkscape:groupmode="layer" |
597 | + id="layer1" |
598 | + transform="translate(268,-635.29076)" |
599 | + style="display:inline"> |
600 | + <path |
601 | + style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)" |
602 | + d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z" |
603 | + id="path6455" |
604 | + inkscape:connector-curvature="0" |
605 | + sodipodi:nodetypes="sssssssss" /> |
606 | + </g> |
607 | + <g |
608 | + inkscape:groupmode="layer" |
609 | + id="layer3" |
610 | + inkscape:label="PLACE YOUR PICTOGRAM HERE" |
611 | + style="display:inline" /> |
612 | + <g |
613 | + inkscape:groupmode="layer" |
614 | + id="layer2" |
615 | + inkscape:label="BADGE" |
616 | + style="display:none" |
617 | + sodipodi:insensitive="true"> |
618 | + <g |
619 | + style="display:inline" |
620 | + transform="translate(-340.00001,-581)" |
621 | + id="g4394" |
622 | + clip-path="none"> |
623 | + <g |
624 | + id="g855"> |
625 | + <g |
626 | + inkscape:groupmode="maskhelper" |
627 | + id="g870" |
628 | + clip-path="url(#clipPath873)" |
629 | + style="opacity:0.6;filter:url(#filter891)"> |
630 | + <path |
631 | + transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)" |
632 | + d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z" |
633 | + sodipodi:ry="12" |
634 | + sodipodi:rx="12" |
635 | + sodipodi:cy="552.36218" |
636 | + sodipodi:cx="252" |
637 | + id="path844" |
638 | + style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
639 | + sodipodi:type="arc" /> |
640 | + </g> |
641 | + <g |
642 | + id="g862"> |
643 | + <path |
644 | + sodipodi:type="arc" |
645 | + style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
646 | + id="path4398" |
647 | + sodipodi:cx="252" |
648 | + sodipodi:cy="552.36218" |
649 | + sodipodi:rx="12" |
650 | + sodipodi:ry="12" |
651 | + d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z" |
652 | + transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" /> |
653 | + <path |
654 | + transform="matrix(1.25,0,0,1.25,33,-100.45273)" |
655 | + d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z" |
656 | + sodipodi:ry="12" |
657 | + sodipodi:rx="12" |
658 | + sodipodi:cy="552.36218" |
659 | + sodipodi:cx="252" |
660 | + id="path4400" |
661 | + style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
662 | + sodipodi:type="arc" /> |
663 | + <path |
664 | + sodipodi:type="star" |
665 | + style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" |
666 | + id="path4459" |
667 | + sodipodi:sides="5" |
668 | + sodipodi:cx="666.19574" |
669 | + sodipodi:cy="589.50385" |
670 | + sodipodi:r1="7.2431178" |
671 | + sodipodi:r2="4.3458705" |
672 | + sodipodi:arg1="1.0471976" |
673 | + sodipodi:arg2="1.6755161" |
674 | + inkscape:flatsided="false" |
675 | + inkscape:rounded="0.1" |
676 | + inkscape:randomized="0" |
677 | + d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z" |
678 | + transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" /> |
679 | + </g> |
680 | + </g> |
681 | + </g> |
682 | + </g> |
683 | +</svg> |
684 | diff --git a/interfaces/.empty b/interfaces/.empty |
685 | new file mode 100644 |
686 | index 0000000..792d600 |
687 | --- /dev/null |
688 | +++ b/interfaces/.empty |
689 | @@ -0,0 +1 @@ |
690 | +# |
691 | diff --git a/layer.yaml b/layer.yaml |
692 | new file mode 100644 |
693 | index 0000000..3399b95 |
694 | --- /dev/null |
695 | +++ b/layer.yaml |
696 | @@ -0,0 +1,2 @@ |
697 | +includes: |
698 | + - 'layer:basic' |
699 | diff --git a/layers/.empty b/layers/.empty |
700 | new file mode 100644 |
701 | index 0000000..792d600 |
702 | --- /dev/null |
703 | +++ b/layers/.empty |
704 | @@ -0,0 +1 @@ |
705 | +# |
706 | diff --git a/lib/AdvancedRoutingHelper.py b/lib/AdvancedRoutingHelper.py |
707 | new file mode 100644 |
708 | index 0000000..73526a5 |
709 | --- /dev/null |
710 | +++ b/lib/AdvancedRoutingHelper.py |
711 | @@ -0,0 +1,132 @@ |
712 | +"""Routing module.""" |
713 | +import errno |
714 | +import os |
715 | +import subprocess |
716 | + |
717 | +from RoutingEntry import RoutingEntryType |
718 | + |
719 | +from RoutingValidator import RoutingConfigValidator |
720 | + |
721 | +from charmhelpers.core import hookenv |
722 | +from charmhelpers.core.host import ( |
723 | + CompareHostReleases, |
724 | + lsb_release, |
725 | +) |
726 | + |
727 | + |
728 | +class AdvancedRoutingHelper: |
729 | + """Helper class for routing.""" |
730 | + |
731 | + if_script = '95-juju_routing' |
732 | + common_location = '/usr/local/sbin/' # trailing slash |
733 | + net_tools_up_path = '/etc/network/if-up.d/' # trailing slash |
734 | + net_tools_down_path = '/etc/network/if-down.d/' # trailing slash |
735 | + netplan_up_path = '/etc/networkd-dispatcher/routable.d/' # trailing slash |
736 | + netplan_down_path = '/etc/networkd-dispatcher/off.d/' # trailing slash |
737 | + policy_routing_service_path = '/etc/systemd/system/' # trailing slash |
738 | + ifup_path = None |
739 | + ifdown_path = None |
740 | + |
741 | + def __init__(self): |
742 | + """Init function.""" |
743 | + hookenv.log('Init {}'.format(self.__class__.__name__), level=hookenv.INFO) |
744 | + self.pre_setup() |
745 | + |
746 | + def pre_setup(self): |
747 | + """Create folder path for the ifup/down scripts.""" |
748 | + if not os.path.exists(self.common_location + 'if-up/'): |
749 | + os.makedirs(self.common_location + 'if-up/') |
750 | + hookenv.log('Created {}'.format(self.common_location + 'if-up/'), level=hookenv.INFO) |
751 | + if not os.path.exists(self.common_location + 'if-down/'): |
752 | + os.makedirs(self.common_location + 'if-down/') |
753 | + hookenv.log('Created {}'.format(self.common_location + 'if-down/'), level=hookenv.INFO) |
754 | + |
755 | + self.ifup_path = '{}if-up/{}'.format(self.common_location, self.if_script) |
756 | + self.ifdown_path = '{}if-down/{}'.format(self.common_location, self.if_script) |
757 | + |
758 | + # check for service file of charm-policy-routing, and block if its present |
759 | + if os.path.exists(self.policy_routing_service_path + 'charm-pre-install-policy-routing.service'): |
760 | + hookenv.log('It looks like charm-policy-routing is enabled. ' |
761 | + ' charm-pre-install-policy-routing.service', 'WARNING') |
762 | + hookenv.status_set('blocked', 'Please disable charm-policy-routing') |
763 | + raise Exception('Please disable charm-policy-routing') |
764 | + |
765 | + def post_setup(self): |
766 | + """Symlinks the up/down scripts from the if.up/down or netplan scripts location.""" |
767 | + hookenv.log('Symlinking into distro specific network manager', level=hookenv.INFO) |
768 | + release = lsb_release()['DISTRIB_CODENAME'].lower() |
769 | + if CompareHostReleases(release) < "bionic": |
770 | + self.symlink_force(self.ifup_path, '{}{}'.format(self.net_tools_up_path, self.if_script)) |
771 | + self.symlink_force(self.ifdown_path, '{}{}'.format(self.net_tools_down_path, self.if_script)) |
772 | + else: |
773 | + self.symlink_force(self.ifup_path, '{}{}'.format(self.netplan_up_path, self.if_script)) |
774 | + self.symlink_force(self.ifdown_path, '{}{}'.format(self.netplan_down_path, self.if_script)) |
775 | + |
776 | + def setup(self): |
777 | + """Modify the interfaces configurations.""" |
778 | + RoutingConfigValidator() |
779 | + |
780 | + hookenv.log('Writing {}'.format(self.ifup_path), level=hookenv.INFO) |
781 | + # Modify if-up.d |
782 | + with open(self.ifup_path, 'w') as ifup: |
783 | + ifup.write("# This file is managed by Juju.\n") |
784 | + ifup.write("ip route flush cache\n") |
785 | + for entry in RoutingEntryType.entries: |
786 | + ifup.write(entry.addline) |
787 | + os.chmod(self.ifup_path, 0o755) |
788 | + |
789 | + hookenv.log('Writing {}'.format(self.ifdown_path), level=hookenv.INFO) |
790 | + # Modify if-down.d |
791 | + with open(self.ifdown_path, 'w') as ifdown: |
792 | + ifdown.write("# This file is managed by Juju.\n") |
793 | + for entry in list(reversed(RoutingEntryType.entries)): |
794 | + ifdown.write(entry.removeline) |
795 | + ifdown.write("ip route flush cache\n") |
796 | + os.chmod(self.ifdown_path, 0o755) |
797 | + |
798 | + self.post_setup() |
799 | + |
800 | + def apply_routes(self): |
801 | + """Apply the new routes to the system.""" |
802 | + hookenv.log('Applying routing rules', level=hookenv.INFO) |
803 | + for entry in RoutingEntryType.entries: |
804 | + entry.apply() |
805 | + |
806 | + def remove_routes(self): |
807 | + """Cleanup job.""" |
808 | + hookenv.log('Removing routing rules', level=hookenv.INFO) |
809 | + if os.path.exists(self.ifdown_path): |
810 | + try: |
811 | + subprocess.check_call(["sh", "-c", self.ifdown_path], shell=True) |
812 | + except subprocess.CalledProcessError: |
813 | + # Either rules are removed or not valid |
814 | + hookenv.log('ifdown script failed. Maybe rules are already gone?', 'WARNING') |
815 | + |
816 | + # remove files |
817 | + if os.path.exists(self.ifup_path): |
818 | + os.remove(self.ifup_path) |
819 | + if os.path.exists(self.ifdown_path): |
820 | + os.remove(self.ifdown_path) |
821 | + |
822 | + # remove symlinks |
823 | + release = lsb_release()['DISTRIB_CODENAME'].lower() |
824 | + try: |
825 | + if CompareHostReleases(release) < "bionic": |
826 | + os.remove('{}{}'.format(self.net_tools_up_path, self.if_script)) |
827 | + os.remove('{}{}'.format(self.net_tools_down_path, self.if_script)) |
828 | + else: |
829 | + os.remove('{}{}'.format(self.netplan_up_path, self.if_script)) |
830 | + os.remove('{}{}'.format(self.netplan_down_path, self.if_script)) |
831 | + except Exception: |
832 | + hookenv.log('Nothing to clean up', 'WARNING') |
833 | + |
834 | + def symlink_force(self, target, link_name): |
835 | + """Ensures accute symlink by removing any existing links.""" |
836 | + try: |
837 | + os.symlink(target, link_name) |
838 | + except OSError as e: |
839 | + if e.errno == errno.EEXIST: |
840 | + os.remove(link_name) |
841 | + os.symlink(target, link_name) |
842 | + else: |
843 | + raise e |
844 | diff --git a/lib/RoutingEntry.py b/lib/RoutingEntry.py |
845 | new file mode 100644 |
846 | index 0000000..a6d6ed8 |
847 | --- /dev/null |
848 | +++ b/lib/RoutingEntry.py |
849 | @@ -0,0 +1,221 @@ |
850 | +"""RoutingEntry Classes. |
851 | + |
852 | +This module contains the following abstract type and |
853 | +concrete implementations, thta model a routing table |
854 | + |
855 | + RoutingEntryType |
856 | + --------------------------------------- |
857 | + | | | |
858 | + RoutingEntryTable RoutingEntryRoute RoutingEntryRule |
859 | +""" |
860 | +import subprocess |
861 | + |
862 | +from charmhelpers.core import hookenv |
863 | + |
864 | + |
865 | +class RoutingEntryType(object): |
866 | + """Abstract type RoutingEntryType.""" |
867 | + |
868 | + entries = [] # static <RoutingEntryType>[] |
869 | + config = None # config entry |
870 | + |
871 | + def __init__(self): |
872 | + """Constructor.""" |
873 | + hookenv.log('Init {}'.format(self.__class__.__name__), level=hookenv.INFO) |
874 | + pass |
875 | + |
876 | + def exec_cmd(self, cmd, pipe=False): |
877 | + """Runs a subprocess and returns True or False on success.""" |
878 | + try: |
879 | + if pipe: |
880 | + hookenv.log('Subprocess check shell: {} {}'.format(self.__class__.__name__, cmd), level=hookenv.INFO) |
881 | + command = ' '.join(cmd) |
882 | + ps = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
883 | + ps.communicate() |
884 | + return True if ps.returncode == 0 else False |
885 | + else: |
886 | + hookenv.log('Subprocess check: {} {}'.format(self.__class__.__name__, cmd), level=hookenv.INFO) |
887 | + subprocess.check_call(cmd) |
888 | + return True |
889 | + except subprocess.CalledProcessError as error: |
890 | + hookenv.log(error, level=hookenv.ERROR) |
891 | + return False |
892 | + |
893 | + @staticmethod |
894 | + def add_rule(newrule): |
895 | + """Due to config-change/install etc hooks etc. |
896 | + |
897 | + The validator may be called multiple times |
898 | + The static list will get duplicate items added |
899 | + """ |
900 | + for rule in RoutingEntryType.entries: |
901 | + if rule.addline == newrule.addline: |
902 | + return |
903 | + RoutingEntryType.entries.append(newrule) |
904 | + |
905 | + def apply(self): |
906 | + """Not implemented, should override in strategy.""" |
907 | + raise NotImplementedError |
908 | + |
909 | + def create_line(self): |
910 | + """Not implemented, should override in strategy.""" |
911 | + raise NotImplementedError |
912 | + |
913 | + @property |
914 | + def addline(self): |
915 | + """Not implemented, should override in strategy.""" |
916 | + raise NotImplementedError |
917 | + |
918 | + @property |
919 | + def removeline(self): |
920 | + """Not implemented, should override in strategy.""" |
921 | + raise NotImplementedError |
922 | + |
923 | + |
924 | +class RoutingEntryTable(RoutingEntryType): |
925 | + """Concrete RoutingEntryType Strategy.""" |
926 | + |
927 | + table_name_file = '/etc/iproute2/rt_tables.d/juju-managed.conf' |
928 | + table_index_counter = 100 # static |
929 | + tables = [] |
930 | + |
931 | + def __init__(self, config): |
932 | + """Adds unique tables to the tables list.""" |
933 | + hookenv.log('Created {}'.format(self.__class__.__name__), level=hookenv.INFO) |
934 | + super().__init__() |
935 | + self.config = config |
936 | + |
937 | + if self.config['table'] not in RoutingEntryTable.tables: |
938 | + RoutingEntryTable.tables.append(self.config['table']) |
939 | + |
940 | + def create_line(self): |
941 | + """Not implemented in this base class.""" |
942 | + raise NotImplementedError |
943 | + |
944 | + def apply(self): |
945 | + """Opens iproute tables and adds the known list of tables into this file.""" |
946 | + with open(RoutingEntryTable.table_name_file, 'w') as rt_table_file: |
947 | + num = RoutingEntryTable.table_index_counter |
948 | + for tbl in RoutingEntryTable.tables: |
949 | + rt_table_file.write("{} {}\n".format(num, tbl)) |
950 | + num += 1 |
951 | + |
952 | + @property |
953 | + def addline(self): |
954 | + """Returns the add line for the ifup script.""" |
955 | + return "# Table: name {}\n".format(self.config['table']) |
956 | + |
957 | + @property |
958 | + def removeline(self): |
959 | + """Returns the remove line for the ifdown script.""" |
960 | + line = "ip route flush table {}\n".format(self.config['table']) |
961 | + line += "ip rule del table {}\n".format(self.config['table']) |
962 | + return line |
963 | + |
964 | + |
965 | +class RoutingEntryRoute(RoutingEntryType): |
966 | + """Concrete RoutingEntryType Strategy.""" |
967 | + |
968 | + def __init__(self, config): |
969 | + """Nothing special in this constructor.""" |
970 | + hookenv.log('Created {}'.format(self.__class__.__name__), level=hookenv.INFO) |
971 | + super().__init__() |
972 | + self.config = config |
973 | + |
974 | + def create_line(self): |
975 | + """Creates and returns the command line for this rule object.""" |
976 | + cmd = ["ip", "route", "replace"] |
977 | + |
978 | + # default route in table |
979 | + if 'default_route' in self.config.keys(): |
980 | + cmd.append("default") |
981 | + cmd.append("via") |
982 | + cmd.append(self.config['gateway']) |
983 | + cmd.append("table") |
984 | + cmd.append(self.config['table']) |
985 | + if 'device' in self.config.keys(): |
986 | + cmd.append("dev") |
987 | + cmd.append(self.config['device']) |
988 | + # route in any given table or none |
989 | + else: |
990 | + cmd.append(self.config['net']) |
991 | + if 'gateway' in self.config.keys(): |
992 | + cmd.append("via") |
993 | + cmd.append(self.config['gateway']) |
994 | + if 'device' in self.config.keys(): |
995 | + cmd.append("dev") |
996 | + cmd.append(self.config['device']) |
997 | + if 'table' in self.config.keys(): |
998 | + cmd.append("table") |
999 | + cmd.append(self.config['table']) |
1000 | + if 'metric' in self.config.keys(): |
1001 | + cmd.append("metric") |
1002 | + cmd.append(str(self.config['metric'])) |
1003 | + |
1004 | + return cmd |
1005 | + |
1006 | + def apply(self): |
1007 | + """Applies this rule object to the system.""" |
1008 | + super().exec_cmd(self.create_line()) |
1009 | + |
1010 | + @property |
1011 | + def addline(self): |
1012 | + """Returns the add line for the ifup script.""" |
1013 | + return ' '.join(self.create_line()) + "\n" |
1014 | + |
1015 | + @property |
1016 | + def removeline(self): |
1017 | + """Returns the remove line for the ifdown script.""" |
1018 | + return ' '.join(self.create_line()).replace(" replace ", " del ") + "\n" |
1019 | + |
1020 | + |
1021 | +class RoutingEntryRule(RoutingEntryType): |
1022 | + """Concrete RoutingEntryType Strategy.""" |
1023 | + |
1024 | + def __init__(self, config): |
1025 | + """Nothing special in this constructor.""" |
1026 | + hookenv.log('Created {}'.format(self.__class__.__name__), level=hookenv.INFO) |
1027 | + super().__init__() |
1028 | + self.config = config |
1029 | + |
1030 | + def create_line(self): |
1031 | + """Creates and returns the command line for this rule object.""" |
1032 | + cmd = ["ip", "rule", "add", "from", self.config['from-net']] |
1033 | + |
1034 | + if 'to-net' in self.config.keys(): |
1035 | + cmd.append("to") |
1036 | + cmd.append(self.config['to-net']) |
1037 | + cmd.append("lookup") |
1038 | + if 'table' in self.config.keys(): |
1039 | + cmd.append(self.config['table']) |
1040 | + else: |
1041 | + cmd.append('main') |
1042 | + else: |
1043 | + if 'table' in self.config.keys(): |
1044 | + cmd.append(self.config['table']) |
1045 | + if 'priority' in self.config.keys(): |
1046 | + cmd.append(self.config['priority']) |
1047 | + |
1048 | + return cmd |
1049 | + |
1050 | + def apply(self): |
1051 | + """Applies this rule object to the system.""" |
1052 | + if self.is_duplicate() is False: |
1053 | + # ip rule replace not supported, check for duplicates |
1054 | + super().exec_cmd(self.create_line()) |
1055 | + |
1056 | + @property |
1057 | + def addline(self): |
1058 | + """Returns the add line for the ifup script.""" |
1059 | + return ' '.join(self.create_line()) + "\n" |
1060 | + |
1061 | + @property |
1062 | + def removeline(self): |
1063 | + """Returns the remove line for the ifdown script.""" |
1064 | + return ' '.join(self.create_line()).replace(" add ", " del ") + "\n" |
1065 | + |
1066 | + def is_duplicate(self): |
1067 | + """Ip rule add does not prevent duplicates in older kernel versions.""" |
1068 | + # https://patchwork.ozlabs.org/patch/624553/ |
1069 | + parts = ' '.join(self.create_line()).split("add ") |
1070 | + return self.exec_cmd(["ip", "rule", "|", "grep", "\"" + parts[1] + "\""], pipe=True) |
1071 | diff --git a/lib/RoutingValidator.py b/lib/RoutingValidator.py |
1072 | new file mode 100644 |
1073 | index 0000000..24d780c |
1074 | --- /dev/null |
1075 | +++ b/lib/RoutingValidator.py |
1076 | @@ -0,0 +1,182 @@ |
1077 | +"""RoutingValidator Class. |
1078 | + |
1079 | +Validates the entire json configuration constructing a model. |
1080 | +""" |
1081 | +import ipaddress |
1082 | +import json |
1083 | +import pprint |
1084 | +import re |
1085 | +import subprocess |
1086 | + |
1087 | +from RoutingEntry import ( |
1088 | + RoutingEntryRoute, |
1089 | + RoutingEntryRule, |
1090 | + RoutingEntryTable, |
1091 | + RoutingEntryType, |
1092 | +) |
1093 | + |
1094 | +from charmhelpers.core import hookenv |
1095 | + |
1096 | + |
1097 | +class RoutingConfigValidator: |
1098 | + """Validates the enitre json configuration constructing model of rules.""" |
1099 | + |
1100 | + def __init__(self): |
1101 | + """Init function.""" |
1102 | + hookenv.log('Init {}'.format(self.__class__.__name__), level=hookenv.INFO) |
1103 | + |
1104 | + self.pattern = re.compile("^([a-zA-Z0-9-]+)$") |
1105 | + self.tables = [] |
1106 | + self.config = self.read_configurations() |
1107 | + self.verify_config() |
1108 | + |
1109 | + def read_configurations(self): |
1110 | + """Read and parse the JSON configuration file.""" |
1111 | + json_decoded = [] |
1112 | + conf = hookenv.config() |
1113 | + |
1114 | + if conf['advanced-routing-config']: |
1115 | + try: |
1116 | + json_decoded = json.loads(conf['advanced-routing-config']) |
1117 | + hookenv.log('Read json config from juju config', level=hookenv.INFO) |
1118 | + except ValueError: |
1119 | + hookenv.status_set('blocked', 'JSON format invalid.') |
1120 | + raise Exception('JSON format invalid.') |
1121 | + else: |
1122 | + hookenv.status_set('blocked', 'JSON invalid or not set in charm config') |
1123 | + raise Exception('JSON format invalid.') |
1124 | + return json_decoded |
1125 | + |
1126 | + def verify_config(self): |
1127 | + """Iterates the entries in the config checking each type for sanity.""" |
1128 | + hookenv.log('Verifying json config', level=hookenv.INFO) |
1129 | + type_order = ['table', 'route', 'rule'] |
1130 | + |
1131 | + for entry_type in type_order: |
1132 | + # get all the tables together first, so that we can provide strict relations |
1133 | + for conf in self.config: |
1134 | + if 'type' not in conf: |
1135 | + hookenv.status_set('blocked', 'Bad config: \'type\' not found in routing entry') |
1136 | + hookenv.log('type not found in rule', level=hookenv.ERROR) |
1137 | + raise Exception('type not found in rule') |
1138 | + if entry_type == "table" and entry_type == conf['type']: |
1139 | + self.verify_table(conf) |
1140 | + if entry_type == "route" and entry_type == conf['type']: |
1141 | + self.verify_route(conf) |
1142 | + if entry_type == "rule" and entry_type == conf['type']: |
1143 | + self.verify_rule(conf) |
1144 | + |
1145 | + def verify_table(self, conf): |
1146 | + """Verify rules.""" |
1147 | + hookenv.log('Verifying table \n{}'.format(pprint.pformat(conf)), level=hookenv.INFO) |
1148 | + if 'table' not in conf: |
1149 | + hookenv.status_set('blocked', 'Bad network config: \'table\' missing in rule') |
1150 | + raise Exception('Bad network config: \'table\' missing in rule') |
1151 | + |
1152 | + if self.pattern.match(conf['table']) is False: |
1153 | + hookenv.status_set('blocked', 'Bad network config: garbage table name in table [0-9a-zA-Z-]') |
1154 | + raise Exception('Bad network config: garbage table name in table [0-9a-zA-Z-]') |
1155 | + |
1156 | + if conf['table'] in self.tables: |
1157 | + hookenv.status_set('blocked', 'Bad network config: duplicate table name') |
1158 | + raise Exception('Bad network config: duplicate table name') |
1159 | + |
1160 | + self.tables.append(conf['table']) |
1161 | + RoutingEntryType.add_rule(RoutingEntryTable(conf)) |
1162 | + |
1163 | + def verify_route(self, conf): |
1164 | + """Verify routes.""" |
1165 | + hookenv.log('Verifying route \n{}'.format(pprint.pformat(conf)), level=hookenv.INFO) |
1166 | + try: |
1167 | + ipaddress.ip_address(conf['gateway']) |
1168 | + except ValueError as error: |
1169 | + hookenv.log('Bad gateway IP: {} - {}'.format(conf['gateway'], error), level=hookenv.INFO) |
1170 | + hookenv.status_set('blocked', 'Bad gateway IP: {} - {}'.format(conf['gateway'], error)) |
1171 | + raise Exception('Bad gateway IP: {} - {}'.format(conf['gateway'], error)) |
1172 | + |
1173 | + try: |
1174 | + ipaddress.ip_network(conf['net']) |
1175 | + except ValueError as error: |
1176 | + hookenv.log('Bad network config: {} - {}'.format(conf['net'], error), level=hookenv.INFO) |
1177 | + hookenv.status_set('blocked', 'Bad network config: {} - {}'.format(conf['net'], error)) |
1178 | + raise Exception('Bad network config: {} - {}'.format(conf['net'], error)) |
1179 | + |
1180 | + if 'table' in conf: |
1181 | + if self.pattern.match(conf['table']) is False: |
1182 | + hookenv.log('Bad network config: garbage table name in rule [0-9a-zA-Z]', level=hookenv.INFO) |
1183 | + hookenv.status_set('blocked', 'Bad network config') |
1184 | + raise Exception('Bad network config: garbage table name in rule [0-9a-zA-Z]') |
1185 | + if conf['table'] not in self.tables: |
1186 | + hookenv.log('Bad network config: table reference not defined', level=hookenv.INFO) |
1187 | + hookenv.status_set('blocked', 'Bad network config') |
1188 | + raise Exception('Bad network config: table reference not defined') |
1189 | + |
1190 | + if 'default_route' in conf: |
1191 | + if isinstance(conf['default_route'], bool): |
1192 | + hookenv.log('Bad network config: default_route should be bool', level=hookenv.INFO) |
1193 | + hookenv.status_set('blocked', 'Bad network config') |
1194 | + raise Exception('Bad network config: default_route should be bool') |
1195 | + if 'table' not in conf: |
1196 | + hookenv.log('Bad network config: replacing the default route in main table blocked', level=hookenv.INFO) |
1197 | + hookenv.status_set('blocked', 'Bad network config') |
1198 | + raise Exception('Bad network config: replacing the default route in main table blocked') |
1199 | + |
1200 | + if 'device' in conf: |
1201 | + if self.pattern.match(conf['device']) is False: |
1202 | + hookenv.log('Bad network config: garbage device name in rule [0-9a-zA-Z-]', level=hookenv.INFO) |
1203 | + hookenv.status_set('blocked', 'Bad network config') |
1204 | + raise Exception('Bad network config: garbage device name in rule [0-9a-zA-Z-]') |
1205 | + try: |
1206 | + subprocess.check_call(["ip", "link", "show", conf['device']]) |
1207 | + except subprocess.CalledProcessError as error: |
1208 | + hookenv.log('Device {} does not exist'.format(conf['device']), level=hookenv.INFO) |
1209 | + hookenv.status_set('blocked', 'Bad network config') |
1210 | + raise Exception('Device {} does not exist, {}'.format(conf['device'], error)) |
1211 | + |
1212 | + if 'metric' in conf: |
1213 | + try: |
1214 | + int(conf['metric']) |
1215 | + except ValueError: |
1216 | + hookenv.log('Bad network config: metric expected to be integer', level=hookenv.INFO) |
1217 | + hookenv.status_set('blocked', 'Bad network config') |
1218 | + raise Exception('Bad network config: metric expected to be integer') |
1219 | + |
1220 | + RoutingEntryType.add_rule(RoutingEntryRoute(conf)) |
1221 | + |
1222 | + def verify_rule(self, conf): |
1223 | + """Verify rules.""" |
1224 | + hookenv.log('Verifying rule \n{}'.format(pprint.pformat(conf)), level=hookenv.INFO) |
1225 | + |
1226 | + try: |
1227 | + ipaddress.ip_network(conf['from-net']) |
1228 | + except ValueError as error: |
1229 | + hookenv.status_set('blocked', 'Bad network config: {} - {}'.format(conf['from-net'], error)) |
1230 | + raise Exception('Bad network config: {} - {}'.format(conf['from-net'], error)) |
1231 | + |
1232 | + if 'to-net' in conf: |
1233 | + try: |
1234 | + ipaddress.ip_network(conf['to-net']) |
1235 | + except ValueError as error: |
1236 | + hookenv.status_set('blocked', 'Bad network config: {} - {}'.format(conf['to-net'], error)) |
1237 | + raise Exception('Bad network config: {} - {}'.format(conf['to-net'], error)) |
1238 | + |
1239 | + if 'table' not in conf: |
1240 | + hookenv.status_set('blocked', 'Bad network config: \'table\' missing in rule') |
1241 | + raise Exception('Bad network config: \'table\' missing in rule') |
1242 | + |
1243 | + if conf['table'] not in self.tables: |
1244 | + hookenv.status_set('blocked', 'Bad network config: table reference not defined') |
1245 | + raise Exception('Bad network config: table reference not defined') |
1246 | + |
1247 | + if self.pattern.match(conf['table']) is False: |
1248 | + hookenv.status_set('blocked', 'Bad network config: garbage table name in rule [0-9a-zA-Z-]') |
1249 | + raise Exception('Bad network config: garbage table name in rule [0-9a-zA-Z-]') |
1250 | + |
1251 | + if 'priority' in conf: |
1252 | + try: |
1253 | + int(conf['priority']) |
1254 | + except ValueError: |
1255 | + hookenv.status_set('blocked', 'Bad network config: priority expected to be integer') |
1256 | + raise Exception('Bad network config: priority expected to be integer') |
1257 | + |
1258 | + RoutingEntryType.add_rule(RoutingEntryRule(conf)) |
1259 | diff --git a/metadata.yaml b/metadata.yaml |
1260 | new file mode 100644 |
1261 | index 0000000..8984ae6 |
1262 | --- /dev/null |
1263 | +++ b/metadata.yaml |
1264 | @@ -0,0 +1,16 @@ |
1265 | +name: advanced-routing |
1266 | +maintainer: David O Neill <david.o.neill@canonical.com> |
1267 | +summary: Routing configuration editor |
1268 | +description: | |
1269 | + This charm can be deployed alongside principle charms to enable custom |
1270 | + network routes management. |
1271 | +subordinate: true |
1272 | +series: |
1273 | + - xenial |
1274 | + - bionic |
1275 | +tags: |
1276 | + - misc |
1277 | +requires: |
1278 | + juju-info: |
1279 | + interface: juju-info |
1280 | + scope: container |
1281 | diff --git a/reactive/advanced_routing.py b/reactive/advanced_routing.py |
1282 | new file mode 100644 |
1283 | index 0000000..c1ddbb1 |
1284 | --- /dev/null |
1285 | +++ b/reactive/advanced_routing.py |
1286 | @@ -0,0 +1,56 @@ |
1287 | +"""Reactive charm hooks.""" |
1288 | +from charmhelpers.core.hookenv import ( |
1289 | + Hooks, |
1290 | + config, |
1291 | + status_set, |
1292 | +) |
1293 | + |
1294 | +from charms.reactive import ( |
1295 | + hook, |
1296 | + set_flag, |
1297 | + when, |
1298 | + when_not, |
1299 | +) |
1300 | + |
1301 | +from AdvancedRoutingHelper import AdvancedRoutingHelper |
1302 | + |
1303 | +hooks = Hooks() |
1304 | +advanced_routing = AdvancedRoutingHelper() |
1305 | + |
1306 | + |
1307 | +@when_not('advanced-routing.installed') |
1308 | +def install_routing(): |
1309 | + """Install the charm.""" |
1310 | + if config('enable-advanced-routing'): |
1311 | + advanced_routing.setup() |
1312 | + advanced_routing.apply_routes() |
1313 | + set_flag('advanced-routing.installed') |
1314 | + |
1315 | + status_set('active', 'Unit is ready.') |
1316 | + |
1317 | + |
1318 | +@hook('upgrade-charm') |
1319 | +def upgrade_charm(): |
1320 | + """Handle resource-attach and config-changed events.""" |
1321 | + if config('enable-advanced-routing'): |
1322 | + status_set('maintenance', 'Installing new static routes.') |
1323 | + advanced_routing.remove_routes() |
1324 | + advanced_routing.setup() |
1325 | + advanced_routing.apply_routes() |
1326 | + |
1327 | + status_set('active', 'Unit is ready.') |
1328 | + |
1329 | + |
1330 | +@when('config.changed.enable-advanced-routing') |
1331 | +def reconfigure_routing(): |
1332 | + """Handle routing configuration change.""" |
1333 | + if config('enable-advanced-routing'): |
1334 | + status_set('maintenance', 'Installing routes.') |
1335 | + advanced_routing.remove_routes() |
1336 | + advanced_routing.setup() |
1337 | + advanced_routing.apply_routes() |
1338 | + else: |
1339 | + status_set('maintenance', 'Removing routes.') |
1340 | + advanced_routing.remove_routes() |
1341 | + |
1342 | + status_set('active', 'Unit is ready.') |
1343 | diff --git a/requirements.txt b/requirements.txt |
1344 | new file mode 100644 |
1345 | index 0000000..6dae81c |
1346 | --- /dev/null |
1347 | +++ b/requirements.txt |
1348 | @@ -0,0 +1,3 @@ |
1349 | +# Include python requirements here |
1350 | +charmhelpers |
1351 | +charms.reactive |
1352 | \ No newline at end of file |
1353 | diff --git a/revision b/revision |
1354 | new file mode 100644 |
1355 | index 0000000..d00491f |
1356 | --- /dev/null |
1357 | +++ b/revision |
1358 | @@ -0,0 +1 @@ |
1359 | +1 |
1360 | diff --git a/tests/bundles/bionic.yaml b/tests/bundles/bionic.yaml |
1361 | new file mode 100644 |
1362 | index 0000000..65ab84b |
1363 | --- /dev/null |
1364 | +++ b/tests/bundles/bionic.yaml |
1365 | @@ -0,0 +1,45 @@ |
1366 | +series: bionic |
1367 | +description: "charm-advanced-routing test bundle" |
1368 | +machines: |
1369 | + "0": |
1370 | + constraints: "arch=amd64" |
1371 | +relations: |
1372 | + - - "testhost:juju-info" |
1373 | + - "charm-advanced-routing:juju-info" |
1374 | +applications: |
1375 | + testhost: |
1376 | + charm: "ubuntu" |
1377 | + options: |
1378 | + hostname: "testhost" |
1379 | + num_units: 1 |
1380 | + to: |
1381 | + - "0" |
1382 | + charm-advanced-routing: |
1383 | + charm: "../.." |
1384 | + series: bionic |
1385 | + num_units: 0 |
1386 | + options: |
1387 | + advanced-routing-config: |- |
1388 | + [ { |
1389 | + "type": "table", |
1390 | + "table": "SF1" |
1391 | + }, { |
1392 | + "type": "route", |
1393 | + "default_route": true, |
1394 | + "net": "192.170.1.0/24", |
1395 | + "gateway": "10.191.86.2", |
1396 | + "table": "SF1", |
1397 | + "metric": 101, |
1398 | + "device": "eth0" |
1399 | + }, { |
1400 | + "type": "route", |
1401 | + "net": "6.6.6.0/24", |
1402 | + "gateway": "10.191.86.2" |
1403 | + }, { |
1404 | + "type": "rule", |
1405 | + "from-net": "192.170.2.0/24", |
1406 | + "to-net": "192.170.2.0/24", |
1407 | + "table": "SF1", |
1408 | + "priority": 101 |
1409 | + } ] |
1410 | + enable-advanced-routing: true |
1411 | diff --git a/tests/bundles/overlays/bionic.yaml.j2 b/tests/bundles/overlays/bionic.yaml.j2 |
1412 | new file mode 100644 |
1413 | index 0000000..5da69ca |
1414 | --- /dev/null |
1415 | +++ b/tests/bundles/overlays/bionic.yaml.j2 |
1416 | @@ -0,0 +1,2 @@ |
1417 | +applications: |
1418 | + {{ charm_name }}: |
1419 | diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py |
1420 | new file mode 100644 |
1421 | index 0000000..1b22cb8 |
1422 | --- /dev/null |
1423 | +++ b/tests/functional/conftest.py |
1424 | @@ -0,0 +1,71 @@ |
1425 | +#!/usr/bin/python3 |
1426 | +""" |
1427 | +Reusable pytest fixtures for functional testing. |
1428 | + |
1429 | +Environment variables |
1430 | +--------------------- |
1431 | + |
1432 | +test_preserve_model: |
1433 | +if set, the testing model won't be torn down at the end of the testing session |
1434 | +""" |
1435 | + |
1436 | +import asyncio |
1437 | +import os |
1438 | +import subprocess |
1439 | +import uuid |
1440 | + |
1441 | +from juju.controller import Controller |
1442 | + |
1443 | +from juju_tools import JujuTools |
1444 | + |
1445 | +import pytest |
1446 | + |
1447 | + |
1448 | +@pytest.fixture(scope='module') |
1449 | +def event_loop(): |
1450 | + """Override the default pytest event loop. |
1451 | + |
1452 | + Do this too allow for fixtures using a broader scope. |
1453 | + """ |
1454 | + loop = asyncio.get_event_loop_policy().new_event_loop() |
1455 | + asyncio.set_event_loop(loop) |
1456 | + loop.set_debug(True) |
1457 | + yield loop |
1458 | + loop.close() |
1459 | + asyncio.set_event_loop(None) |
1460 | + |
1461 | + |
1462 | +@pytest.fixture(scope='module') |
1463 | +async def controller(): |
1464 | + """Connect to the current controller.""" |
1465 | + _controller = Controller() |
1466 | + await _controller.connect_current() |
1467 | + yield _controller |
1468 | + await _controller.disconnect() |
1469 | + |
1470 | + |
1471 | +@pytest.fixture(scope='module') |
1472 | +async def model(controller): |
1473 | + """Live only for the duration of the test.""" |
1474 | + model_name = "functest-{}".format(str(uuid.uuid4())[-12:]) |
1475 | + _model = await controller.add_model(model_name, |
1476 | + cloud_name=os.getenv('PYTEST_CLOUD_NAME'), |
1477 | + region=os.getenv('PYTEST_CLOUD_REGION'), |
1478 | + ) |
1479 | + # https://github.com/juju/python-libjuju/issues/267 |
1480 | + subprocess.check_call(['juju', 'models']) |
1481 | + while model_name not in await controller.list_models(): |
1482 | + await asyncio.sleep(1) |
1483 | + yield _model |
1484 | + await _model.disconnect() |
1485 | + if not os.getenv('PYTEST_KEEP_MODEL'): |
1486 | + await controller.destroy_model(model_name) |
1487 | + while model_name in await controller.list_models(): |
1488 | + await asyncio.sleep(1) |
1489 | + |
1490 | + |
1491 | +@pytest.fixture(scope='module') |
1492 | +async def jujutools(controller, model): |
1493 | + """Juju tools.""" |
1494 | + tools = JujuTools(controller, model) |
1495 | + return tools |
1496 | diff --git a/tests/functional/juju_tools.py b/tests/functional/juju_tools.py |
1497 | new file mode 100644 |
1498 | index 0000000..5e4a1cc |
1499 | --- /dev/null |
1500 | +++ b/tests/functional/juju_tools.py |
1501 | @@ -0,0 +1,69 @@ |
1502 | +"""Juju tools.""" |
1503 | +import base64 |
1504 | +import pickle |
1505 | + |
1506 | +import juju |
1507 | + |
1508 | +# from juju.errors import JujuError |
1509 | + |
1510 | + |
1511 | +class JujuTools: |
1512 | + """Juju tools.""" |
1513 | + |
1514 | + def __init__(self, controller, model): |
1515 | + """Init.""" |
1516 | + self.controller = controller |
1517 | + self.model = model |
1518 | + |
1519 | + async def run_command(self, cmd, target): |
1520 | + """Run a command on a unit. |
1521 | + |
1522 | + :param cmd: Command to be run |
1523 | + :param unit: Unit object or unit name string |
1524 | + """ |
1525 | + unit = ( |
1526 | + target |
1527 | + if isinstance(target, juju.unit.Unit) |
1528 | + else await self.get_unit(target) |
1529 | + ) |
1530 | + action = await unit.run(cmd) |
1531 | + return action.results |
1532 | + |
1533 | + async def remote_object(self, imports, remote_cmd, target): |
1534 | + """Run command on target machine and returns a python object of the result. |
1535 | + |
1536 | + :param imports: Imports needed for the command to run |
1537 | + :param remote_cmd: The python command to execute |
1538 | + :param target: Unit object or unit name string |
1539 | + """ |
1540 | + python3 = "python3 -c '{}'" |
1541 | + python_cmd = ('import pickle;' |
1542 | + 'import base64;' |
1543 | + '{}' |
1544 | + 'print(base64.b64encode(pickle.dumps({})), end="")' |
1545 | + .format(imports, remote_cmd)) |
1546 | + cmd = python3.format(python_cmd) |
1547 | + results = await self.run_command(cmd, target) |
1548 | + return pickle.loads(base64.b64decode(bytes(results['Stdout'][2:-1], 'utf8'))) |
1549 | + |
1550 | + async def file_stat(self, path, target): |
1551 | + """Run stat on a file. |
1552 | + |
1553 | + :param path: File path |
1554 | + :param target: Unit object or unit name string |
1555 | + """ |
1556 | + imports = 'import os;' |
1557 | + python_cmd = ('os.stat("{}")' |
1558 | + .format(path)) |
1559 | + print("Calling remote cmd: " + python_cmd) |
1560 | + return await self.remote_object(imports, python_cmd, target) |
1561 | + |
1562 | + async def file_contents(self, path, target): |
1563 | + """Return the contents of a file. |
1564 | + |
1565 | + :param path: File path |
1566 | + :param target: Unit object or unit name string |
1567 | + """ |
1568 | + cmd = 'cat {}'.format(path) |
1569 | + result = await self.run_command(cmd, target) |
1570 | + return result['Stdout'] |
1571 | diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt |
1572 | new file mode 100644 |
1573 | index 0000000..f76bfbb |
1574 | --- /dev/null |
1575 | +++ b/tests/functional/requirements.txt |
1576 | @@ -0,0 +1,6 @@ |
1577 | +flake8 |
1578 | +juju |
1579 | +mock |
1580 | +pytest |
1581 | +pytest-asyncio |
1582 | +requests |
1583 | diff --git a/tests/functional/test_routing.py b/tests/functional/test_routing.py |
1584 | new file mode 100644 |
1585 | index 0000000..96a0235 |
1586 | --- /dev/null |
1587 | +++ b/tests/functional/test_routing.py |
1588 | @@ -0,0 +1,54 @@ |
1589 | +#!/usr/bin/python3.6 |
1590 | +"""Main module for functional testing.""" |
1591 | + |
1592 | +import os |
1593 | + |
1594 | +import pytest |
1595 | + |
1596 | +pytestmark = pytest.mark.asyncio |
1597 | +SERIES = ['bionic', 'xenial'] |
1598 | + |
1599 | +############ |
1600 | +# FIXTURES # |
1601 | +############ |
1602 | + |
1603 | + |
1604 | +@pytest.fixture(scope='module', params=SERIES) |
1605 | +async def deploy_app(request, model): |
1606 | + """Deploy the advanced-routing charm as a subordinate of ubuntu.""" |
1607 | + release = request.param |
1608 | + |
1609 | + await model.deploy( |
1610 | + 'ubuntu', |
1611 | + application_name='ubuntu-' + release, |
1612 | + series=release, |
1613 | + channel='stable' |
1614 | + ) |
1615 | + advanced_routing = await model.deploy( |
1616 | + '{}/builds/advanced-routing'.format(os.getenv('JUJU_REPOSITORY')), |
1617 | + application_name='advanced-routing-' + release, |
1618 | + series=release, |
1619 | + num_units=0, |
1620 | + ) |
1621 | + await model.add_relation( |
1622 | + 'ubuntu-' + release, |
1623 | + 'advanced-routing-' + release |
1624 | + ) |
1625 | + |
1626 | + await model.block_until(lambda: advanced_routing.status == 'active') |
1627 | + yield advanced_routing |
1628 | + |
1629 | + |
1630 | +@pytest.fixture(scope='module') |
1631 | +async def unit(deploy_app): |
1632 | + """Return the advanced-routing unit we've deployed.""" |
1633 | + return deploy_app.units.pop() |
1634 | + |
1635 | +######### |
1636 | +# TESTS # |
1637 | +######### |
1638 | + |
1639 | + |
1640 | +async def test_deploy(deploy_app): |
1641 | + """Test the deployment.""" |
1642 | + assert deploy_app.status == 'active' |
1643 | diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py |
1644 | new file mode 100644 |
1645 | index 0000000..fbf31d7 |
1646 | --- /dev/null |
1647 | +++ b/tests/unit/conftest.py |
1648 | @@ -0,0 +1,80 @@ |
1649 | +#!/usr/bin/python3 |
1650 | +"""Configurations for tests.""" |
1651 | +import subprocess |
1652 | +import unittest.mock as mock |
1653 | +import pytest |
1654 | + |
1655 | +from charmhelpers.core.host import ( |
1656 | + CompareHostReleases, |
1657 | + lsb_release, |
1658 | +) |
1659 | + |
1660 | +@pytest.fixture |
1661 | +def mock_layers(monkeypatch): |
1662 | + """Layers mock.""" |
1663 | + import sys |
1664 | + sys.modules['charms.layer'] = mock.Mock() |
1665 | + sys.modules['reactive'] = mock.Mock() |
1666 | + |
1667 | + # Mock any functions in layers that need to be mocked here |
1668 | + def options(layer): |
1669 | + # mock options for layers here |
1670 | + if layer == 'example-layer': |
1671 | + options = {'port': 9999} |
1672 | + return options |
1673 | + else: |
1674 | + return None |
1675 | + |
1676 | + monkeypatch.setattr('AdvancedRoutingHelper.layer.options', options) |
1677 | + |
1678 | + |
1679 | +@pytest.fixture |
1680 | +def mock_hookenv_config(monkeypatch): |
1681 | + """Hookenv mock.""" |
1682 | + import yaml |
1683 | + |
1684 | + def mock_config(): |
1685 | + cfg = {} |
1686 | + yml = yaml.load(open('./config.yaml')) |
1687 | + |
1688 | + # Load all defaults |
1689 | + for key, value in yml['options'].items(): |
1690 | + cfg[key] = value['default'] |
1691 | + |
1692 | + # Manually add cfg from other layers |
1693 | + # cfg['my-other-layer'] = 'mock' |
1694 | + return cfg |
1695 | + |
1696 | + monkeypatch.setattr('AdvancedRoutingHelper.hookenv.config', mock_config) |
1697 | + |
1698 | + |
1699 | +@pytest.fixture |
1700 | +def mock_remote_unit(monkeypatch): |
1701 | + """Remote unit mock.""" |
1702 | + monkeypatch.setattr('AdvancedRoutingHelper.hookenv.remote_unit', lambda: 'unit-mock/0') |
1703 | + |
1704 | + |
1705 | +@pytest.fixture |
1706 | +def mock_charm_dir(monkeypatch): |
1707 | + """Charm dir mock.""" |
1708 | + monkeypatch.setattr('AdvancedRoutingHelper.hookenv.charm_dir', lambda: '/mock/charm/dir') |
1709 | + |
1710 | + |
1711 | +@pytest.fixture |
1712 | +def advanced_routing_helper(tmpdir, mock_hookenv_config, mock_charm_dir, monkeypatch): |
1713 | + """Routing fixture.""" |
1714 | + from AdvancedRoutingHelper import AdvancedRoutingHelper |
1715 | + helper = AdvancedRoutingHelper |
1716 | + |
1717 | + monkeypatch.setattr('AdvancedRoutingHelper.AdvancedRoutingHelper', lambda: helper) |
1718 | + |
1719 | + return helper |
1720 | + |
1721 | +@pytest.fixture |
1722 | +def mock_check_call(monkeypatch): |
1723 | + """Requests.get() mocked to return {'mock_key':'mock_response'}.""" |
1724 | + |
1725 | + def mock_get(*args, **kwargs): |
1726 | + return True |
1727 | + |
1728 | + monkeypatch.setattr(subprocess, "check_call", mock_get) |
1729 | \ No newline at end of file |
1730 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
1731 | new file mode 100644 |
1732 | index 0000000..566d7c1 |
1733 | --- /dev/null |
1734 | +++ b/tests/unit/requirements.txt |
1735 | @@ -0,0 +1,8 @@ |
1736 | +# Include python requirements here |
1737 | +charmhelpers |
1738 | +charms.reactive |
1739 | +mock |
1740 | +pytest |
1741 | +pytest-cov |
1742 | +os-client-config |
1743 | +os-testr>=0.4.1 |
1744 | \ No newline at end of file |
1745 | diff --git a/tests/unit/test_AdvancedRoutingHelper.py b/tests/unit/test_AdvancedRoutingHelper.py |
1746 | new file mode 100644 |
1747 | index 0000000..c24d1dc |
1748 | --- /dev/null |
1749 | +++ b/tests/unit/test_AdvancedRoutingHelper.py |
1750 | @@ -0,0 +1,154 @@ |
1751 | +import os |
1752 | +import pytest |
1753 | +import shutil |
1754 | +import subprocess |
1755 | + |
1756 | +from unittest import mock |
1757 | + |
1758 | +import RoutingValidator |
1759 | + |
1760 | +class TestAdvancedRoutingHelper(): |
1761 | + """Main test class.""" |
1762 | + |
1763 | + test_dir = '/tmp/test/charm-advanced-routing/' # trailing slash |
1764 | + test_ifup_path = test_dir + 'symlink_test/ifup/' |
1765 | + test_ifdown_path = test_dir + 'symlink_test/ifdown/' |
1766 | + test_netplanup_path = test_dir + 'symlink_test/netplanup/' |
1767 | + test_netplandown_path = test_dir + 'symlink_test/netplandown/' |
1768 | + test_script = 'test-script' |
1769 | + |
1770 | + def setup_class(self): |
1771 | + try: |
1772 | + shutil.rmtree(self.test_dir) |
1773 | + except: |
1774 | + pass |
1775 | + |
1776 | + os.makedirs(self.test_dir) |
1777 | + os.makedirs(self.test_ifdown_path) |
1778 | + os.makedirs(self.test_ifup_path) |
1779 | + os.makedirs(self.test_netplandown_path) |
1780 | + os.makedirs(self.test_netplanup_path) |
1781 | + |
1782 | + def test_pytest(self, advanced_routing_helper): |
1783 | + """Simple pytest sanity test.""" |
1784 | + assert True |
1785 | + |
1786 | + def test_constructor(self, advanced_routing_helper): |
1787 | + """No test required""" |
1788 | + pass |
1789 | + |
1790 | + def test_pre_setup(self, advanced_routing_helper): |
1791 | + """Test pre_setup""" |
1792 | + |
1793 | + test_obj = advanced_routing_helper |
1794 | + |
1795 | + test_obj.common_location = self.test_dir |
1796 | + test_obj.if_script = self.test_script |
1797 | + test_obj.policy_routing_service_path = self.test_dir |
1798 | + |
1799 | + try: |
1800 | + os.remove(test_obj.policy_routing_service_path + 'charm-pre-install-policy-routing.service') |
1801 | + except: |
1802 | + pass |
1803 | + |
1804 | + test_obj.pre_setup(test_obj) |
1805 | + |
1806 | + uppath = test_obj.common_location + 'if-up/' |
1807 | + downpath = test_obj.common_location + 'if-down/' |
1808 | + |
1809 | + assert os.path.exists(uppath) |
1810 | + assert os.path.exists(downpath) |
1811 | + |
1812 | + assert test_obj.ifup_path == uppath + test_obj.if_script |
1813 | + assert test_obj.ifdown_path == downpath + test_obj.if_script |
1814 | + |
1815 | + # touch file to cause the exception |
1816 | + try: |
1817 | + with open(test_obj.policy_routing_service_path + 'charm-pre-install-policy-routing.service', "w+") as f: |
1818 | + f.write('dont care\n') |
1819 | + except: |
1820 | + pass # dont care |
1821 | + |
1822 | + with pytest.raises(Exception): |
1823 | + test_obj.pre_setup(test_obj) |
1824 | + |
1825 | + def test_post_setup(self, advanced_routing_helper): |
1826 | + """Test post_setup""" |
1827 | + #test_obj = advanced_routing_helper |
1828 | + assert True |
1829 | + |
1830 | + def test_setup(self, advanced_routing_helper): |
1831 | + """Test setup""" |
1832 | + |
1833 | + def noop(): |
1834 | + pass |
1835 | + |
1836 | + test_obj = advanced_routing_helper |
1837 | + test_obj.common_location = self.test_dir |
1838 | + test_obj.if_script = self.test_script |
1839 | + test_obj.ifup_path = '{}if-up/{}'.format(self.test_dir, self.test_script) |
1840 | + test_obj.ifdown_path = '{}if-down/{}'.format(self.test_dir, self.test_script) |
1841 | + |
1842 | + test_obj.post_setup = noop |
1843 | + RoutingValidator.RoutingConfigValidator.__init__ = mock.Mock(return_value=None) |
1844 | + test_obj.setup(test_obj) |
1845 | + |
1846 | + assert os.path.exists(test_obj.ifup_path) |
1847 | + assert os.path.exists(test_obj.ifdown_path) |
1848 | + |
1849 | + assert True |
1850 | + |
1851 | + def test_apply_routes(self, advanced_routing_helper): |
1852 | + """Test post_setup""" |
1853 | + #test_obj = advanced_routing_helper |
1854 | + assert True |
1855 | + |
1856 | + def test_remove_routes(self, advanced_routing_helper, mock_check_call): |
1857 | + """Test post_setup""" |
1858 | + test_obj = advanced_routing_helper |
1859 | + |
1860 | + with mock.patch('charmhelpers.core.host.lsb_release') as lsbrelBionic: |
1861 | + test_obj.netplan_up_path = self.test_netplanup_path |
1862 | + test_obj.netplan_down_path = self.test_netplandown_path |
1863 | + lsbrelBionic.return_value = "bionic" |
1864 | + test_obj.remove_routes(test_obj) |
1865 | + |
1866 | + with mock.patch('charmhelpers.core.host.lsb_release') as lsbrelArtful: |
1867 | + test_obj.net_tools_up_path = self.test_ifup_path |
1868 | + test_obj.net_tools_down_path = self.test_ifdown_path |
1869 | + lsbrelArtful.return_value = "artful" |
1870 | + test_obj.remove_routes(test_obj) |
1871 | + |
1872 | + assert os.path.exists(test_obj.ifup_path) == False |
1873 | + assert os.path.exists(test_obj.ifdown_path) == False |
1874 | + |
1875 | + assert True |
1876 | + |
1877 | + def test_symlink_force(self, advanced_routing_helper): |
1878 | + """Test symlink_force""" |
1879 | + test_obj = advanced_routing_helper |
1880 | + |
1881 | + target = self.test_dir + 'testfile' |
1882 | + link = self.test_dir + 'testlink' |
1883 | + |
1884 | + try: |
1885 | + os.remove(target) |
1886 | + except: |
1887 | + pass # dont care |
1888 | + |
1889 | + # touch target file to link to |
1890 | + try: |
1891 | + with open(target, "w+") as f: |
1892 | + f.write('dont care\n') |
1893 | + except: |
1894 | + pass # dont care |
1895 | + |
1896 | + assert os.path.exists(target) |
1897 | + |
1898 | + # link it |
1899 | + test_obj.symlink_force(test_obj, target, link) |
1900 | + assert os.path.exists(link) |
1901 | + |
1902 | + # link it again |
1903 | + test_obj.symlink_force(test_obj, target, link) |
1904 | + assert os.path.exists(link) |
1905 | diff --git a/tox.ini b/tox.ini |
1906 | new file mode 100644 |
1907 | index 0000000..5351ddd |
1908 | --- /dev/null |
1909 | +++ b/tox.ini |
1910 | @@ -0,0 +1,46 @@ |
1911 | +[tox] |
1912 | +skipsdist=True |
1913 | +envlist = unit, functional |
1914 | +skip_missing_interpreters = True |
1915 | + |
1916 | +[testenv] |
1917 | +basepython = python3 |
1918 | +setenv = |
1919 | + PYTHONPATH = . |
1920 | + |
1921 | +[testenv:unit] |
1922 | +commands = pytest -o log_cli=true --full-trace -v --ignore {toxinidir}/tests/functional |
1923 | +deps = -r{toxinidir}/tests/unit/requirements.txt |
1924 | + -r{toxinidir}/requirements.txt |
1925 | +setenv = PYTHONPATH={toxinidir}/lib |
1926 | +whitelist_externals = pytest |
1927 | + |
1928 | +[testenv:functional] |
1929 | +passenv = |
1930 | + HOME |
1931 | + JUJU_REPOSITORY |
1932 | + PATH |
1933 | + PYTEST_KEEP_MODEL |
1934 | + PYTEST_CLOUD_NAME |
1935 | + PYTEST_CLOUD_REGION |
1936 | +commands = pytest -v --ignore {toxinidir}/tests/unit |
1937 | +deps = -r{toxinidir}/tests/functional/requirements.txt |
1938 | + -r{toxinidir}/requirements.txt |
1939 | + |
1940 | +[testenv:lint] |
1941 | +commands = flake8 |
1942 | +deps = |
1943 | + flake8 |
1944 | + flake8-docstrings |
1945 | + flake8-import-order |
1946 | + pep8-naming |
1947 | + flake8-colors |
1948 | + |
1949 | +[flake8] |
1950 | +exclude = |
1951 | + .git, |
1952 | + __pycache__, |
1953 | + .tox, |
1954 | +ignore=D401,C901 |
1955 | +max-line-length = 120 |
1956 | +max-complexity = 10 |
See comments inline.