Merge ~dmzoneill/charm-advanced-routing:dev/upgrade into ~dmzoneill/charm-advanced-routing:master

Proposed by David O Neill
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)
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@juju-720cde-24:/home/ubuntu# cat /etc/iproute2/rt_tables.d/juju-managed.conf
  100 SF1

  root@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 cache

  root@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 SF1

To post a comment you must log in.
Revision history for this message
Drew Freiberger (afreiberger) wrote :

See comments inline.

review: Needs Fixing
25b0cd4... by David O Neill

Fixes and improvements from review 371884 MR

https://code.launchpad.net/~dmzoneill/charm-advanced-routing/+git/charm-advanced-routing/+merge/371884

 * 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

Revision history for this message
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

Revision history for this message
Jeremy Lounder (jldev) wrote :

This merge was already completed via an intermediary branch.

review: Approve
Revision history for this message
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

https://code.launchpad.net/~dmzoneill/charm-advanced-routing/+git/charm-advanced-routing/+merge/371884

 * 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 libjuju

  expected config: based on example_config.yaml

  root@juju-720cde-24:/home/ubuntu# cat /etc/iproute2/rt_tables.d/juju-managed.conf
  100 SF1

  root@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 cache

  root@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 SF1

Co-authored-by: Diko Parvanov <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 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+
19diff --git a/LICENSE b/LICENSE
20new file mode 100644
21index 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.
227diff --git a/Makefile b/Makefile
228new file mode 100644
229index 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
282diff --git a/README b/README
283new file mode 100644
284index 0000000..e69de29
285--- /dev/null
286+++ b/README
287diff --git a/README.md b/README.md
288new file mode 100644
289index 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+
342diff --git a/config.yaml b/config.yaml
343new file mode 100644
344index 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
366diff --git a/example_config.yaml b/example_config.yaml
367new file mode 100644
368index 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
399diff --git a/icon.svg b/icon.svg
400new file mode 100644
401index 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>
684diff --git a/interfaces/.empty b/interfaces/.empty
685new file mode 100644
686index 0000000..792d600
687--- /dev/null
688+++ b/interfaces/.empty
689@@ -0,0 +1 @@
690+#
691diff --git a/layer.yaml b/layer.yaml
692new file mode 100644
693index 0000000..3399b95
694--- /dev/null
695+++ b/layer.yaml
696@@ -0,0 +1,2 @@
697+includes:
698+ - 'layer:basic'
699diff --git a/layers/.empty b/layers/.empty
700new file mode 100644
701index 0000000..792d600
702--- /dev/null
703+++ b/layers/.empty
704@@ -0,0 +1 @@
705+#
706diff --git a/lib/AdvancedRoutingHelper.py b/lib/AdvancedRoutingHelper.py
707new file mode 100644
708index 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
844diff --git a/lib/RoutingEntry.py b/lib/RoutingEntry.py
845new file mode 100644
846index 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)
1071diff --git a/lib/RoutingValidator.py b/lib/RoutingValidator.py
1072new file mode 100644
1073index 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))
1259diff --git a/metadata.yaml b/metadata.yaml
1260new file mode 100644
1261index 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
1281diff --git a/reactive/advanced_routing.py b/reactive/advanced_routing.py
1282new file mode 100644
1283index 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.')
1343diff --git a/requirements.txt b/requirements.txt
1344new file mode 100644
1345index 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
1353diff --git a/revision b/revision
1354new file mode 100644
1355index 0000000..d00491f
1356--- /dev/null
1357+++ b/revision
1358@@ -0,0 +1 @@
1359+1
1360diff --git a/tests/bundles/bionic.yaml b/tests/bundles/bionic.yaml
1361new file mode 100644
1362index 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
1411diff --git a/tests/bundles/overlays/bionic.yaml.j2 b/tests/bundles/overlays/bionic.yaml.j2
1412new file mode 100644
1413index 0000000..5da69ca
1414--- /dev/null
1415+++ b/tests/bundles/overlays/bionic.yaml.j2
1416@@ -0,0 +1,2 @@
1417+applications:
1418+ {{ charm_name }}:
1419diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
1420new file mode 100644
1421index 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
1496diff --git a/tests/functional/juju_tools.py b/tests/functional/juju_tools.py
1497new file mode 100644
1498index 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']
1571diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
1572new file mode 100644
1573index 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
1583diff --git a/tests/functional/test_routing.py b/tests/functional/test_routing.py
1584new file mode 100644
1585index 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'
1643diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
1644new file mode 100644
1645index 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
1730diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
1731new file mode 100644
1732index 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
1745diff --git a/tests/unit/test_AdvancedRoutingHelper.py b/tests/unit/test_AdvancedRoutingHelper.py
1746new file mode 100644
1747index 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)
1905diff --git a/tox.ini b/tox.ini
1906new file mode 100644
1907index 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

Subscribers

People subscribed via source and target branches

to all changes: