From 0a393e16324764c285f510bcbcea941fdb4c98e5 Mon Sep 17 00:00:00 2001 From: bravof Date: Tue, 9 Nov 2021 18:10:45 -0300 Subject: [PATCH] feature: sol004 and sol007 new packages Signed-off-by: bravof --- .gitmodules | 6 + SOL004_hackfest_basic_vnf/ChangeLog.txt | 5 + SOL004_hackfest_basic_vnf/Files/icons/osm.png | Bin 0 -> 55888 bytes .../Licenses/license.lic | 201 +++ .../hackfest_basic_vnfd.mf | 14 + .../hackfest_basic_vnfd.yaml | 63 + SOL007_hackfest_basic_ns/ChangeLog.txt | 5 + SOL007_hackfest_basic_ns/Files/icons/osm.png | Bin 0 -> 55888 bytes SOL007_hackfest_basic_ns/Licenses/license.lic | 201 +++ .../hackfest_basic_nsd.mf | 12 + .../hackfest_basic_nsd.yaml | 21 + .../SOL004_k8s_proxy_charm_vnf/ChangeLog.txt | 5 + .../Files/icons/osm.png | Bin 0 -> 55888 bytes .../Licenses/license.lic | 201 +++ .../Scripts/charms/simple/actions.yaml | 54 + .../Scripts/charms/simple/config.yaml | 41 + .../Scripts/charms/simple/hooks/install | 65 + .../Scripts/charms/simple/hooks/start | 65 + .../Scripts/charms/simple/hooks/upgrade-charm | 65 + .../simple/lib/charms/osm/libansible.py | 108 ++ .../charms/simple/lib/charms/osm/ns.py | 301 ++++ .../simple/lib/charms/osm/proxy_cluster.py | 59 + .../charms/simple/lib/charms/osm/sshproxy.py | 375 +++++ .../Scripts/charms/simple/lib/ops/__init__.py | 20 + .../Scripts/charms/simple/lib/ops/charm.py | 575 ++++++++ .../charms/simple/lib/ops/framework.py | 1067 ++++++++++++++ .../charms/simple/lib/ops/jujuversion.py | 98 ++ .../charms/simple/lib/ops/lib/__init__.py | 194 +++ .../Scripts/charms/simple/lib/ops/log.py | 51 + .../Scripts/charms/simple/lib/ops/main.py | 348 +++++ .../Scripts/charms/simple/lib/ops/model.py | 1237 +++++++++++++++++ .../Scripts/charms/simple/lib/ops/storage.py | 318 +++++ .../Scripts/charms/simple/lib/ops/testing.py | 586 ++++++++ .../Scripts/charms/simple/lib/ops/version.py | 50 + .../Scripts/charms/simple/metadata.yaml | 28 + .../Scripts/charms/simple/mod/charms.osm | 1 + .../Scripts/charms/simple/mod/operator | 1 + .../Scripts/charms/simple/src/charm.py | 65 + .../Scripts/cloud_init/cloud-config.txt | 12 + .../k8s_proxy_charm_vnfd.mf | 362 +++++ .../k8s_proxy_charm_vnfd.yaml | 109 ++ .../SOL007_k8s_proxy_charm_ns/ChangeLog.txt | 5 + .../Files/icons/osm.png | Bin 0 -> 55888 bytes .../Licenses/license.lic | 201 +++ .../k8s_proxy_charm_nsd.mf | 12 + .../k8s_proxy_charm_nsd.yaml | 37 + 46 files changed, 7244 insertions(+) create mode 100644 SOL004_hackfest_basic_vnf/ChangeLog.txt create mode 100644 SOL004_hackfest_basic_vnf/Files/icons/osm.png create mode 100644 SOL004_hackfest_basic_vnf/Licenses/license.lic create mode 100644 SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.mf create mode 100644 SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.yaml create mode 100644 SOL007_hackfest_basic_ns/ChangeLog.txt create mode 100644 SOL007_hackfest_basic_ns/Files/icons/osm.png create mode 100644 SOL007_hackfest_basic_ns/Licenses/license.lic create mode 100644 SOL007_hackfest_basic_ns/hackfest_basic_nsd.mf create mode 100644 SOL007_hackfest_basic_ns/hackfest_basic_nsd.yaml create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/ChangeLog.txt create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Files/icons/osm.png create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Licenses/license.lic create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/actions.yaml create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/config.yaml create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/install create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/start create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/upgrade-charm create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/libansible.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/ns.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/proxy_cluster.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/sshproxy.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/__init__.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/charm.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/framework.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/jujuversion.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/lib/__init__.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/log.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/main.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/model.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/storage.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/testing.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/version.py create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/metadata.yaml create mode 160000 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm create mode 160000 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/src/charm.py create mode 100755 charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/cloud_init/cloud-config.txt create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.mf create mode 100644 charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.yaml create mode 100644 charm-packages/SOL007_k8s_proxy_charm_ns/ChangeLog.txt create mode 100644 charm-packages/SOL007_k8s_proxy_charm_ns/Files/icons/osm.png create mode 100644 charm-packages/SOL007_k8s_proxy_charm_ns/Licenses/license.lic create mode 100644 charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.mf create mode 100644 charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.yaml diff --git a/.gitmodules b/.gitmodules index 3613fb5e..ce1de377 100644 --- a/.gitmodules +++ b/.gitmodules @@ -70,3 +70,9 @@ [submodule "charm-packages/native_manual_scale_charm_vnf/charms/simple/mod/operator"] path = charm-packages/native_manual_scale_charm_vnf/charms/simple/mod/operator url = https://github.com/canonical/operator.git +[submodule "charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator"] + path = charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator + url = https://github.com/canonical/operator +[submodule "charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm"] + path = charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm + url = https://github.com/charmed-osm/charms.osm diff --git a/SOL004_hackfest_basic_vnf/ChangeLog.txt b/SOL004_hackfest_basic_vnf/ChangeLog.txt new file mode 100644 index 00000000..8f45952f --- /dev/null +++ b/SOL004_hackfest_basic_vnf/ChangeLog.txt @@ -0,0 +1,5 @@ + +1.0.0 + +- Package converted with OSM package migration tool. + diff --git a/SOL004_hackfest_basic_vnf/Files/icons/osm.png b/SOL004_hackfest_basic_vnf/Files/icons/osm.png new file mode 100644 index 0000000000000000000000000000000000000000..62012d2a2b491bdcd536d62c3c3c863c0d8c1b33 GIT binary patch literal 55888 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 QVa%F-+q93SJpA+j4_IIKX8-^I literal 0 HcmV?d00001 diff --git a/SOL004_hackfest_basic_vnf/Licenses/license.lic b/SOL004_hackfest_basic_vnf/Licenses/license.lic new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/SOL004_hackfest_basic_vnf/Licenses/license.lic @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.mf b/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.mf new file mode 100644 index 00000000..e3bca28a --- /dev/null +++ b/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.mf @@ -0,0 +1,14 @@ + +vnfd_id: hackfest_basic-vnf +vnfd_product_name: hackfest_basic-vnf +vnfd_provider_id: OSM +vnfd_software_version: 1.0 +vnfd_package_version: 1.0.0 +vnfd_release_date_time: 2021-11-09T18:10:22.368088-03:00 +compatible_specification_versions: 3.3.1 +vnfm_info: OSM + +Source: hackfest_basic_vnfd.yaml +Algorithm: SHA-512 +Hash: 00f939cd8ea49e8dd215ec292c804b2622f1a100e4753e81def28df153186750d23031b8167566194d66459e163e10b2e59aa7b1fd313fcb125850b178a004ac + diff --git a/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.yaml b/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.yaml new file mode 100644 index 00000000..4625e424 --- /dev/null +++ b/SOL004_hackfest_basic_vnf/hackfest_basic_vnfd.yaml @@ -0,0 +1,63 @@ +vnfd: + description: A basic VNF descriptor w/ one VDU + df: + - id: default-df + instantiation-level: + - id: default-instantiation-level + vdu-level: + - number-of-instances: 1 + vdu-id: hackfest_basic-VM + vdu-profile: + - id: hackfest_basic-VM + min-number-of-instances: 1 + ext-cpd: + - id: vnf-cp0-ext + int-cpd: + cpd: vdu-eth0-int + vdu-id: hackfest_basic-VM + id: hackfest_basic-vnf + mgmt-cp: vnf-cp0-ext + product-name: hackfest_basic-vnf + sw-image-desc: + - id: ubuntu18.04 + name: ubuntu18.04 + image: ubuntu18.04 + - id: ubuntu18.04-aws + name: ubuntu18.04-aws + image: ubuntu/images/hvm-ssd/ubuntu-artful-17.10-amd64-server-20180509 + vim-type: aws + - id: ubuntu18.04-azure + name: ubuntu18.04-azure + image: Canonical:UbuntuServer:18.04-LTS:latest + vim-type: azure + - id: ubuntu18.04-gcp + name: ubuntu18.04-gcp + image: ubuntu-os-cloud:image-family:ubuntu-1804-lts + vim-type: gcp + vdu: + - id: hackfest_basic-VM + name: hackfest_basic-VM + sw-image-desc: ubuntu18.04 + alternative-sw-image-desc: + - ubuntu18.04-aws + - ubuntu18.04-azure + - ubuntu18.04-gcp + virtual-compute-desc: hackfest_basic-VM-compute + virtual-storage-desc: + - hackfest_basic-VM-storage + int-cpd: + - id: vdu-eth0-int + virtual-network-interface-requirement: + - name: vdu-eth0 + virtual-interface: + type: PARAVIRT + version: '1.0' + virtual-compute-desc: + - id: hackfest_basic-VM-compute + virtual-cpu: + num-virtual-cpu: "1" + virtual-memory: + size: "1.0" + virtual-storage-desc: + - id: hackfest_basic-VM-storage + size-of-storage: "10" diff --git a/SOL007_hackfest_basic_ns/ChangeLog.txt b/SOL007_hackfest_basic_ns/ChangeLog.txt new file mode 100644 index 00000000..8f45952f --- /dev/null +++ b/SOL007_hackfest_basic_ns/ChangeLog.txt @@ -0,0 +1,5 @@ + +1.0.0 + +- Package converted with OSM package migration tool. + diff --git a/SOL007_hackfest_basic_ns/Files/icons/osm.png b/SOL007_hackfest_basic_ns/Files/icons/osm.png new file mode 100644 index 0000000000000000000000000000000000000000..62012d2a2b491bdcd536d62c3c3c863c0d8c1b33 GIT binary patch literal 55888 zcmeHwcVN_2w*Q%4(|d0OLLi+Mnsh-yL{Y>ZASk;E3aIGrzPH8Qr`y-xuB*GQy6#gH z8?K0`bg9xyNCE*8l8|1f_uuDyCle;aWC)1v`-2OU`L= z(5#u$?!fmFe3mNZ_&Xtc#6$Q}Ju!3PDx%sQo4@$EA1*Fll77=f_4qYo(ZJ)St4mVXJpS17Rb$soNEg~2i(|f=m!2v- zvigS;(kF9=srSseJ$34em8Gd8b4TSYE+{BS9bKGTFmg;$;mF~sh4}>|^YV-H3i5OE z3&s}Yk1fnk?J4OK)i@fza>>%McTAhn(;dzxq(8cP^%Gj zxHvDrFt4yM2T$azdUpBhk~KNYS7o>x@w79obk*XO%br-hY{l|aZntF7il<7WdY9;;EI72}4`5IIr}v(#K1euU>_E z1%B&2@#u=xD^@+aqMt5%Z};0Va5863Z{L2$`}pI2yI!?=`qKbG4@mviTs8mMCrb0~ zC|$MUsg;XMr$3EX$?*5~tCuakgq%Jt@tuChUb<#kKh^k-yP7Z++!z{&aO`C`dxyzO<8CzN~VrgN?s1Z3!M~_;RGopCW(wvfl zkp(#gqn9oz88Le4=rK!22|%1P$GiGvO;1~~_$j6rH0@?BOI9pK^OuHIxVU&}LDAy; zoDroZi*gDIMi=K4FBwsoQ&Ln?Jf^s4)Y7G;BRm~>HFjA?b5||{=PP;4>luERCFrbZ zWXY(dW5$%`j6hF0BStLA&nX_cv?ynE(WsGyg-e$%9yMx-rwc#LT-MdhWveh>pY@u@ zn9TxjyR-DkzE?dr9$O~xs3%HRt}0~;oRIDz!Ct1;%d@zSK*?iE7BhF7xR?o~6hrKD z_0q6Vs{Wr2|#nRQ!l&mbB{0L^qz-Zsy zZr_T&9gNRodGOe>?*J96N}m39h!_}MT`MB|@TmVJA}-D3#gCROf24HD#5~U=_FNbk z?%WvdOI;p{y__1yvxpiCO2(`z5h$o&eBNc(F7@nw{vM{t$DR_NFCLYT0!0R;r>ggD z->M4(SM|Q_TlG@SyOu9oJ+ZK-H=oO1wOEe2S^S+)`AR2_8ac9PcS23CJ3Tes$nsLp1UhtIh4KfzGp|?iyo^M9 zdD-mKmwoM09Ro6 z_}sk#E+2pdKD`01!0_?8djniP0114016+aO<8${0xO@N-`1A(20>j7W?hSDH03`6~ z4R8g9kI&s3;PL@T;L{u63Jf2gyEnk)1CYR{H^3DbK0bGEfXfFUflqIMD=>U~?%n{G z4?qH+-T+r%`1stt0WKea1U|h1uE6l|xqAa#J^%@PdIMa6;p21n2Dp3x68Q86xB|n+ z=k5(~`2ZyF=?!oNhL6wP8{qN*NZ`{O;0g>MpSw4}=-Q@&>x-MnBSef-f!)L33dcC(R! zV-slN%$bxC7D9E^)zonQ+@m&|t!3hrsUL3Lx1Wr%+6N>CoXrl$@MQ?QLzOLK`xvl)AdRXz`*&!gCRLSEtiCASg=*^wjtCkVGsciC97+ zhn;LDgUa099o^xgY@N|K+TPxoX-v-w+huZw$Y$K8ikm)13>=bJM6UBPnXO*m)lpy9 zD89I3$25gn-5HmW+3YYHj$sybivS>zSo}S`#kcF`KaLO)AS#wfh|DJHs=tuh)ZQ_& zTdBR(8Ww$%>ek{|XHuF{idn4C1W`y(5IKQ_NGXuk178=;otViYwTOZ@kD%sLWuzRQ z8+vxh>h0#E2ii>s_LM8znm=^ap4%b{3oVz3MGmIs|EuLch7g{yVtnkK9onwOhOv63 z`XNfpxKlfSu_7cfg(NB^5uoj|I{!QR<5p>xNVuJikMZdRPD{Hxrc+aXaZ+ofQOGJNFt{9h^GD?%`t ziDxJCSWA0rSYz#lhg*w^9}z9Qc}QfzC?G*WE~_0#u#pqIdtgOGjF9HF|D=#H6G)yA zNfK=^na)&@dT0*0Y*vygRTOpm-6F^AJExn^RZZ{O`BklJ*OoWzr^{+E~0rh%gh^)B`J-5z}}2oX!9BnM?2 z*}rG5HFMaD^usknRG7VDyIr7!Yn_K;g@TNgr^sA$p5jX%Cnq#07WDFjBr?5QNw$_I zk_3kcEYAZ*rWpRnG#ZM3^hs)dCzwJe%}zB`SW_igcavG-{Na9cW1FaT{|SwudS|fI zzK$H0N-#zH_aS?}Q-tu0l*<*=U00KI)M)&Te$K)Lq2q3H%K?*-2BrvlK-u@3*_0hB zZQI866m#DqazTytuo$sINg>4(fvB?-R5)6ozP`1ABfXI#Z@q(}Zo3P7iooJdQPbK_ zK&6>PgJ(uKQg52np>Lbi-ckLWvuev3(lxv#>u&o*CUTUEB|RF2Z==`Y{JU!zYLEm} zQG5L8jHZaVJbgLARs%`F9<@bxOD^WET67}*-a`i(C2Kod8nYBq7Bb8Wu1~qn2ywwGh7o2o zD7J3h@~`%J5B@y-mboGqtaE{-T!%4>!2-=^D0IvJkV4Ch5M})9*9=A&HTqWES@9GO{&)O7`|LnB zb(fGbdzPvu_o=7OX-DpDZfY1K5n()6`(8eS5VPiRJwEKi>8h&@`_p?LY>+H_`j+7I z3}{e>zTE+MB2bW6E))6`D;1Z3#rh??O~CH>!^fVN6G4ay&w|~xefANB&%8}wN#1g! zZ%52&9HdB26{y78R1eD@?OjD-HYkryrsSuWf=a%i_RTL*=k`D2ojNf#=t69$0x_!4 zXp&5Py6|w+f}M4pwx=X^6Z*P#7ADn0uzlO2R4QrH*Si&KzxeW9N6GS=wBh04$L3yu zdfFF(-B>Qx(%Gk%k+r^-l793n3Ys^UYzCO#5TY&!dX(F!K2(_$t?ViHaosr`$W zg{G~|RTMq{ckmXt`*%5Puv6@$%$g=^ip==6`RFSnb6j1enB-l)V;RV~w2J}vEs7LY zcUwaP{rju0UNX;rXhv8>r0^nrngl<0;p5a0o&U*isB_yF!tKVNK23%DPEcI8g~B>@ zB-QC*BS2eXNgMG>A%-D7`r*Vj9L9b2HRxf^V)vA5y? z52R*0Nbed8vpS2@Mz8+u@BiIA<+i0!u(}(IO?1^ zovee0P!O~&sSVaRG^IWTGDa2?3k1W?IDZCmA-!MZ^{IywH|vyA>-tY9XhbntK+#TP z7pc+;!IC&w;9KB!wwnbxYMJ!hgo=~@yR)j|)a;_s#iuWS*0=7ZbJ31w@Wg8@8d$rZ zez0!lKP}RYiFd$Pq=U?0Z9}6XlR-}eXJ6#n_h{mg8oHGtDPF2Yd?18gxVW3DOl|bb z)aeu|Rg#_gae$%_NoYfRC2i|EM~}pgq&!(TwSt$Qa~kM`-}|*ZqqW@rk6)$|BR*vlNLwF23>) zGF3&xd6^3Y_)LJF##mliT@sQVey!?%?ns}KQ#88PFhIAE=r`XS_cx|1UXwyFcU-6m zfB(?&eIbvo%oITuiQpzTnGNLpY7^ytxQXVT>7-&sECNy?WW@}J%MdOU2~`?fFklB2 zXyZxmunx#qq!z<7ix)M4A}x?JkT7UVOj59-MtvJ?pceXg@^ET+U?KVorLG-YNHaVS zz#K+)1dCcee1~H1UvfqDTcS~u?(hNX+PQ^7CryXpSwuB2{0hjNBRDG^-K|vpi@%UE zaXyK7-`LC8mm7496pP(O%0quWT2iQ-dE5MZnk)m5*HSeAd$omGmD6UU&pzMuTg9@c zGua}y1>;4&*hnMZ`iSnW=%V7#R1!zVk(nH%XA0<{TR7AOpz^5=fE}nlVgw@4sW4Qo z13Dlx8*NaYLPMiysVjzV-`Y%X9eAEz>^wq-Gv%=0V<_j79k3gQfZgcHP=10m!}0}R zmhU?YK`h9v=6Bx$b#zk9{iP%ek0A5;29m=QpdOMfP?t0$k|O4eqLvS~k~VK1+VZbl zutQS;)!2}~H2(M}it{t}6y)x|A&3@&`FFo(Lo*-dh=El)&@ zWn|c-nWRWC!A7YCi|${YW~8V=MJ+=fnEcDv{{1J5zEjX1ef#%5mx@{V4zRF(&D3CM zX%TNYTl4da5t#%Hps00H8@s_gCb(7JpP_X{{HsI zNAA3J>Z@kNeEKU=NBO{H2*0kGeMCcs)2^>~E-_D?HCpkvzta!?vYzgX$RS&#jC27I z;TeF?4iIuZixiUzVI}%!(_ukD*f{(lYPA_CLaIT$qlKP&WI0v5^lMUNWRR`3iB!-A zofeD%fNc5b9kRAGQ~Z+U#6B_)GT8NEtSA$-GFfC~?{ywGdjT}r`6Si8PuAudl1Ih| zMv4uI9ccyh$)U>URiCck9GjSS)(V#IuSjN1zKQJ6h_3V@7)kY~E9l_fz3QW((XU7E zJskCmclOaOu{mUfgUxXTP07L9wcLqheRe@thx)2M6Lc9Lp>K_E_Qdgpbpo)j2*_X$IAc>zfbV^?=z zH)KvONy8$5yuO;C2+D~pHk`Wm9tV{m+2=}=jPFMdQ8%61oiIKtYF%hV46&BV*(=^3 zX**JG22vDoch@#FPW#`t-h8cK$mo>6it;Ei4lZt{imQOBhh>3VTun8)9)`%fAOdU-Ium~=&N`!CL=A;Pe{>rANLqGm0?QTCy_5be$ihcM8kQI-UDmDF* z?IhH|AE@p&P>rGNxS%R~Bg*rL6E+VUonplX!I%}vmG}-|WMcv-;OnbToO|cdGg}tj zIeXaaCd=hiA~qn`XdoY>-hlk1hCH%4QY=;QwlY=VrDd2YS5sJSSRm|KZN1jQNL2$F zn>)|aM%X70#N<`>HCBl}XN3 zZ!TCCGm`F)E2M+^i?juX+O*IN%2Y*?86-Inw#D<7Z3$*w*Fqy)j}h`t(^0{eSr#{1 zm=xT9e}yN3OeCe-B8CdWUXEvFg9bxB_^;tYUYPN883gy;m%D*_s<545Yww`?mwycx z{~-j!-a|V5RjI>Fy5t#|Z*Tm|qH#HCzh_&;yCB8Re>}nS5M$BUZK5wf z-SCG}8gyS44P zKm}Yp&#?;KYB$ik&Btj7C}Kv~AlM?%>ewE+Lg7j9Slk#|5Sc>~$ci7mzln~I8B5{8 z@V$A-4tALc>M_`sEFAax^Yy_4?SQ>vI(J6snSES_vP~2^;So@k8q@$Q-p{yPm<+1$ z1Uh^s=o-dBd0ABb_eU+PX7BE< zFUO-8YH;V)O)w^N1cR{Oz#86PiUbOKYcnY10JN@IaI*}D7yB65J8Ge^g&>9!4)}o+ z_$WLutH~CWqK?v?tj!sev`-4Q$Xtj&l2!LE794v1Vj>*l*m`j9c*&*{@6C-FCgIsw zW@w@eXfr&+S%%oq*n%{0ETvSSJUXAL;>jD0P`F( z(Ee1vhgP23NR?>w4E$c7w3Sn}xr6>IWh!MTBLrK9nV09ojsASGhXZKTe~LefMM+$;}*s*)g=kQdxg zMwaStV3aC=m@s^+dJuytgnAM+a*->KCP!M%|E)`Ja&~ld5PzgK2T%I#R|?W?G3k`$ z>%V+yMO2~OgqL8WkY9!^V>xCwS{CXuJ76%(jmV}ZxS_x7I72%+&(nhF97>eKj|D_B zMR8%uVIVTnJoSYe_`_{56_`M2Z#5n9T<-losH>-HLmQcuHkh9XxAr79@L1)jCq|yB z2p=_oz%}4S#$Kb`JhkGS>^mvj4ge@FW~0fip-Dqxqk3Ps_W&d2?85c$e6{9INpb-prH`#qHoq7dRvc@X5_>DS zk6C_%K-0J{I^R93Tgxd%uBDqo(u8j#R8nR=>?o9=Ew0bnmI4CO1w?l`jG(eEItP{V zA`Cmf?aUPV%~i}c42QP|X3m%H+P z2h(%TERF3Hd2}d(R&cOFaq_6Yvalp1@TJ4;KN4=1(=bxkkt8G*&kjb!Gm~UsafkMN za8GPR>*mDNL4X0+paGFO-=c)gvA3#hY+AkT*(^8+^*zb7%x-w5Gev}p@S9mZv);SD zaNlKvED57W<44gU%<6Yrj#G*vlx_(h454o)1KMM@#7wWw+C_(8OLS5wHB$uH#K|-~ zavTk9xBWgEBMsEiPU1(Tgv<=xl0iq{!3+O2{5WHC@frBC=3$WDo@x4gwqP zrjDpcI$B*L+9Ee=V31z1If{* zJRup$6L+|MR9?ci&p^CS00UNkF4`wze8FRqsdS z^aQ-cA#~>SncK=lnt4-Z-14!oM8Y@ei+OQ30xhS%*!}d%;5@kj!1okpHB9m*bi!mG zs4#0|5*P_tkp}_J_QLCphv=iWGTPf+3)ZHiF_EL_yeo=S@*>I)8%OzyNaEOsQlz57 zl+o3v9N*Z@UDiR|4G}UaE;m19c1r^7PcP?Iki+ zeI2!{BjQ)0KmYktP>5aX^l@}45yw7q=FHS_wurgvV5L9}oxUZVLSg*dZANSooE0JVxW^mY3tq<7M?c|-5qWipy>QLEL+ z8ddB-rh0>3uNM~E45V+*GfvEOhYjK$h*LVn6dDpy*AgG|YLiC!>X(O(Y|uHaw|buC z@2*{2Nq_ho3Yjp4Ts}Ep-p_fC1x-rCDXPqEg7NWY=ANhA-!4pW7bX!Kks;%jkg@dw z#mJhE4j-0n=lLX-Yt_imbAOaUTGEN^qVli5`R{STDSeRPX_keqp!W|99|Gdkm_cQx zMmlaokV&Eyno1AKqD*ZD5TSz*M^H;p8f4U@Z=mDJU;TCF+!>odj{3a3ymduIMeoAj z(_r_?PV|iDYQ8-airZ0v&5+Sby0(rGe{I-1+dUq4f46bGUR(hwSo+@b&&CPh6rX)4dSzS5hhZjR@lcKFleMp>A+D;#xj2jvLT+78a zEHP6GOW;5qpq{?YJ+EO6_>kM+w&LUC2`o&|`*Cg4`t|F_dp9D{Xa%>4lM^lmSVTym z6GFP+A_FUUo)sl%lx+@eUUN%TusS%bn-d{>3#+GfM=O4?Kr>WeOT9OG4p>79f-q)H z%e$WUa|%!p7xvPewY1T@dJ83nK0ph?#)1kg6eSO*!NIv?feXPK848h-PU()2e2U>G z6Gl$$w1Ji8=H^lhCJax6>rogX>|xEx$r0EdFV+bU4}YQ*qF}wQ+rTy&MMXss%Z%%V5YJFqoAIzeeA{Qtm{FRMk?~gf(IZhYx7=RhTyX!b zoQQoj#R&l*0{6gRucn%yg}H(dB6h_jrz9nQBOROvD;0&!_S2t#cIOxhi|o%K!GPiT zgVm2ZsDf?r@Ah&ksr*owDYFvppr?kvO6-j?!@dYqfFU?GA)ee`#a-w6nEg@uLfOhZahs>p6SP*HxXcJNRk5Y=Bj@9WGt$O+4e zeOTZgS5ug6NrIxtW(~tsCIi*NctL>PPqDt!5S;x3%DiHf|AhH$zSzD#R z&UirW)^5Q7G+hB1OdVG6(F5u)-j56kJ$mjeY`&htz+!%Xy(z@gK7VuDoDC8k9ZiD< z!8kJ-wjVln;DReVCz-C0kkH!>6cU#WdZ%1-^%R2sWvXC0Qh6cd{K3;CEkAV%J|Gdb zn)Q*%9nNC4Rta`>$@b{&ln0N0ObPtFFJVe?ZD>$DQELSrv{Fv!r~zplaxti*Ym3v8 z(fNCqRdbn2+X#Duvm>upVed7wXHL#zzRzkt3wQ|j=*gpld4O*RVI z&B5tA-bD?n*`})@t7a;FH1P#G(YYVbKrbJ_%Q)a^Js)-CqaoRYkII~v2Y9#*){IzX z6)af>8ji2Fa62r}8=ITyvrU`&jEyIl6&?$d39}UlOK@4=t!k*dLy-oG@p8@k>yzUm zb}U&i*SC;5n`KDg@PofLq_FPm8=6DDX{}dEhvp84ks_gs4GrV76cPOjY@s3GiVZ>` zN2;e~1ytKAODlbD4yQX)ABJ;KLK(0z)}DWvD!Y&2I9ouDzkw*kD%#(^l_XQluNMv< zkFdReLCUjQ4tEZ-7HC5hg!SrITR40giX?C&(s--^SP^Stu^RQx>Wj6VG$<=n@B#AJG&f14XU?A!lG)W+KuCWBWJRtf?Bv%;=?R}VH#PT}wZ3&& zxHDzc*Voh3sZ(haW;s(#pu$wa0OseSV`GFzIiVm>;XdptWs~dhz6-8tGeQBJehUY) zI8Un+TZBjvIc37VGK0IkIUc7|B&yR$g@5bEU^JhLYE zx)lx^aOlLHy?N^1S=6qUshtUEEaFc@pdWpBQZ)#lBbXr&QcY#WnvIO<8 zr%~*mKf(k(ZjiC(SHY&_zzPH7rkidOfZZRJFd~Kx8%Co>jS_Zi7$`DWfoN@Q z4L|wm$Hk4Wy+Ro;{Dqz?D)}m z7cEIc;^E~5HDaA3yHhw$FCd9&$Olop8tdet!)@Ki>P^y%9c^^>;)TQ_TLiBKxB|_= zUysHuXxs$jZXN7{$w><#BQCcyoY2R^9Q%+VTZJ>?rQuOX=FX?~P3w_$vH|&Bxyb*WOpfj*At)tQAcYkUM5hUv zl5*@26g3FcgEUu&1-4g&B@F`W2Q(me!d0XTmIWd7WJYYlA&L%{B5Uso9FUm^r$Lg?Y2z^?44DI`nrcCaRdY%>39wg*W4 zeY3&HGn%)<;KJ;S-66ak5(Ab)&{tcyEg*R}sL+D;6hR}$QuICdLs+vV&i*H~2*mJ= z=Oqu?>|y|WKJvTrNTP`#R|2y$j0N|3m6b!+gmlyfd}ESyHDXn89hPEu3CNKnuT2aD zUHt3?8j%x{aqHaqkFkv3;0GFSBYxH*7F zs)@jgIW35(Lny$e%Z_DxKn^3uX)z%GYb!a5YXUAvE2qv8ZNc8wfD=82@co~9@4r=o}n0g=LNDmQElozb15I>R}NSEW!VoQ`KT zM>nQ~#9J(QSq8$j76TI9*uLQ6QEB0qy4L1;>kUpy+*8+OJtv8Xh~#}ze2Z8vrIxkp zpj}M>km0BEvCZ7iV@fh2`ydku-XU=a`zC?nz-=Mjpo{W`MhOqE(nYQ#6>Vq1Ueqo+M~xyQ=1 z^hDBl`ssx|G%;!#O^BWa`Qav0&vX91*=oC;a?rD(9J@slP~Gs3h=S{=@gW&W^IOwl_f7fN0!3KlvB1=N=!i#(Kdv}1%z!j6nu9eH# znfLR7j#yA)l2jVn}l&HFsDe1g!*b+Y=2Zi7#k&72;*Y+ zP6t-m4zRFI!YZ)80>`0>?TJACC4co;b}$laAU9am<-!w*ljcIgA}J;|fm~u_$e6L) z0{l&Emv9itQ&^GkHdH;=J>Me4-HlPP5StcZS5u}GiAG6{e|r<5{$ZpD2?urfeh$ruY4tD`3 z#j*SdMKr7d226qm%Yk9B479jaUnNbCn@6X*4)>{sSjUN^q@=I=iwZIYFd%)AB>)pV zec!%)LPU}64)4M<+Kw5nkYNWRu#}XnOp1#NMY<8}pdMMk-;7j$8XjMh0lO7?bH{P? z56%K)sHyei_o(}b6^lJb0~uGAo!(>eJ?8u^J2pf}Y*CUZRDl?i9MN2vOnU6tu@92c zQjEo2+P8lgQKUX409>4XUrjJ%hxckuw6*%2dU(xgekZ4WRRknCjKzJ0r2fV$hl zRA9tlrgwLn;kPGO zW6Kt5{p1t{jePhjwW{8x`y4p&4vNZSlqnyHBq(H{U_F0nK>Z3u|cv3_)w>)CdP~V8$mf=byU%>kcG~RHIL|iOe2u6FoY8o;IA4;;S?XaSV7O{zXq*H zODf42^k7k#O?BAg82PtOOhK>00B>(^r`KP9U2r0@--?mpK?oy^0qsGYSJlf8G1FO& zSU497z?MQLup+=z*8~>}7jO3q-)m}WqEE`o7FA!UU1P_l(Apg2i%!XZ4Ic1;61+>WIVy8GIJ&lGA&LUm3>clztr#ws4m`a!; zSimP>X~BzW(Yr&EJ5SGjOrPS#|}Z()|XkZ6SCAUl_9zhuB2JWcOsQ z{ms7Dj+Y-)vA|`ko8<`ur2!k-c+XsS8`tkIBZhrl>}p}pFuPk~V`B-)4q|A3%YAz? zR&EoPt%1x0l4Iu}Z*0cP4A>tfI1F*j4n?-WJMA=wFhEH;cx8 zEn+B7ow_E+@rHwDskSE^nv|MS-d-sEhqqy4dzK~NW#RTau_t4@-bS4U6Qw4`Lv1E&d|d1f$gm!dpnC-DBn&Em@ysrD zc6k6n>>PAr^_UahTXwVa`VijWOh`qCymC_uHo?L3%oX%UKfwyC6GDUgTHd0p(3^3L zsS0z>BxyE|nV8=u^=LQ}2-m~%uHCnE8AfWcfZp2gkvBjK}RfzL~1*k$6T4WWv_zghOMvFIr_Bv>bg4JGHfyMOx0J6jzK z7Tx+hQwD209wF{|_Ph9IYu*OdW5Aj~WpD(QmmME%8Mbhu(zcb%XSNGeBf~yeQFpwE zS+5an4-wcd2c9Gxh+^R+urap^EkFmrXKDip;Xh0Sz=_$L1B;D}6`O_Eh*D+FyC(?^b`={zL(337VI1o&PZS?C4Yt4M`yF-a8yaHwkf$ z@d!X=BN(--`3oxRIz&a`6A&pJD!AJ{bvSb7!~hssmrw$|#bu(TkbJtZP4tWRMPE?z zu#o3HF%GY;uInLBGR|gUC?C@|#K!6P4S!AVq z8G;OK3KvGIa7qQJ%e|!SQY&^KB_%AMYPLClv9tPpCFI|affS@lNiX{~10E}X zuC^$#^ATs5a|Q}Z1ZMMutx$B`8da z4TzZ)NkH~YuHMDwGEiz*At~!M&wZ|Ya%^i`Ya-HqK7h^e96!hP7!gc8SEGS(9iCJh zzS>D$SefMj`-PSN(__buKgP`lHH)m_5ByW1fDPgjk}ZI2^wr-6WjvMv7ld!k4aX>Z zOF88QKZyOouoti}8g~m4Owu-66*ZgBl#3gqS-?YN_k1K+c@#7(?Sl=*a|Y-_y?ru| z3xSrPceHsm)qDH01C|}^yBZZSNuU^JYj2$UJ=N&X&_!c8ZL0qTC`u>r(7u(C$pNG~ zMK)1wjyXkMz-xfKvfN0wEMs@ z$cnPao~QA{BMAyAX0h8hb@i0jU#}t=mQ%o}PTsVgCTQ*>hX7I^#)h5eEXwBkq?EXC z!b2lTnvjy%r@ff8#PeStKlj(R^WCe}@ZT_-Von)}JrS)|WVYkqKnt@mDU8R-F>{2} z(mz%Fgtj((B2>3Iy6LX8KjWC&yEI5xD3Dd*P#|Kc%X$&@6|SSl4{73C67iEloyhf# zqdj}}(B#QeKrLQ;^$LUHrF&|n3?XG_lq@!TO=#*Jw{`A+8}e9!o#TGsB_FQP&;W09 zVuKk=#1pXmZnEeua$c9b}hMw zhK7Q>2}lXWCIMj*=;Gj~0|x_BJ}hCdv1H94#8P5=yVBXP?gPN*`FIH(Ko8_gs62sY zBs@!1-8-Nh+^cBXb@b!LL3#U2L zwK=O41@j_;m%R!o?SSY0Z_(KMdlWj7+tfo6Q zO18fGJj*sLSUq$4wC5dqzxhQSS+T!cckR?M zYY+;sx*gEC>WwwDF!cxc9l4*<%V+*TN*T74?NR%D+p!|gi|rK!)+1Ut?t=Hs4wAlSF>*zvE-!9#w9q%)_8{c(C@W<$4Ti9Y4F z1#PJlQk2ny;E}NO2yOJp?u|aQ={t%6oO@U%D?extfeNzk`4^2i=AuGv2|2Jnjn(}A ziUsRI96EP3~k*zw)MaEe_g$9L}ZHah!lLkMqfdRswu!` z;CKM%V6i{!z3Nve3<$hCWvRf9jCLdCh33*X*bhXj#?msMY;i_Jr?nBPaul4GDj1h; zb<^s??|o>JDb)JI=_=!(J?q~6pPIJz=21K_4pc#|`~oYYl~D<4`)loyt7*tBqbMp% zsR{*~#CEP6>GNid{@!`u*P!jeViz`;L8~IH?=WK%9?j;DXhv%hC5DU=R9j}({S`sG zvQ(2Ixp}=tg*_^Nm()W40q-U1pKIZ;@Y<6(1g$OFDu{|+Unb1!R0B9eozo&t5aQ+vE0VCSS1 z7H^_zM2xQ55aWH1kB^pBJ{OVyv`ATy3%3g>K%fYg4cE7bu-7pAwOZD%5hx=v_f8so zo@kV49;w6P;nfa2S%uMHfoq5QXzkFnh>vQjFZ8PM($v({en-o$Z{>r^6@UBV#xW~o zBa+gQr5LPhgp1z&Yia%}vNeCI>xhu~J~`$#O4km7ZE`6QfLUFvGKEsH`AV0y3EC7w zp#nnTynKYomxP`5|?k0e9*UDPyl;z8NrzGY@3MG z!PIr+J#3dzB|I9c-%GOw{fkW40@R&W%c@HMg}HzZ@ep-E-77V)?$tRx_ z)?084!uP|j=|O4f`lF(b6K#9T78Zx4Np^RYkwcL~k#hWcr57Ms1yv%}zOQu~7VLaO zwQbkujVK!IJE$^<|sYl<$jpc5=W~;({BT?E)c&0cRqAvn6gg{%^rkt4MN(q*+Vj zST}!M`~iosTRN%fe@n@Hb`Q1~-$^Mo7Mk8zL?g9xDGZc8BxEv;k9ky>JdLJPbY8bd z7>@>&FNX{iEPmYe6rPN&x>}06<=mQSvnFp=tFRd;eqPq|fgR!&CY}oZzIg-8v9qVY z`s>p#D}LOa`lmMC7U~F_gy^4Ch#GcSTY-#s;pNSN3x0~=*Yd~%-753B}5iyK* zx9)|tGeaPCCglzb2CE(hWO%6N@)qo1nPKuIdNA`XYKKgNf=iLgbm2jjEP|q4!zr%~ zF8t;?sz0!ws^u5RiXW@A$HXIWjH#muBg4LTsC)MtLLF96G)42U5*E8RSn&HF2x=?M z&=8s$NKscyU7ZFJBrV{YB^pWbVOV})@1hZr3sD~|6!n?Xd1`T_iWhljqqA1Z-Kt<5(tW{n3B{l;YM98bM9i|-@zwk6h z(w!euY6TJvu>WC@ID*0@aTF@ThWp}p(kgDHWATZ!rQ!FK9XgXT!=?fmo#+w~Rj*Vi z_WQBD5iCd4QISMx+A-7siaFG}mZDXI1>`8jp;V>YV2_`zTceSO82u?!Qf51xcwO zbRHdQLk0}SJ3Hn_NOc_tHNnk-SzSlVm>?W#79$qaP7Bjtr8rFvR?Kz_$nfpYb1wMe zptx`lE?El|Sd&j}mh)86xrO#O{gX`MR>SOzv#U35{JdAQ^3>&T(U2jSVFM_P6h@x# z?Q*8q)m|vW&-^M&2d@#lyTQ^>icny4V7S`ZGG{x4lOUMiy8}{qNxUErn9;;DjP-O4 zV)E})zlNpQ!vS0)b=w-jm!}HCeE`D>_y`MG_`;xA=mC%~LA~7+E>9NZ5BtkHtq|%M zr&_|^2%Z3awId$4&1I%j9h;~iV*VAVw*R59J!OV${o~1-$fYnF=FPoh4C47G{dugV z_uhMN|1J64VRwb#%67RLUwh-VExKCMxWdqJp+=~oddwUh_^J#5V4!!yRd2u`*wY&% z3#BMUEQP{A43c7VNB}_&M6oM{+q)%U9<4h3C{^n!C@*3z4ThlnHid!Atj}m~r_)_q zF|H;GSENu@$PDxuBp}-i|eJ% zSh`>lHfpGR;DHA!Il;m&La-^=mD=lbp8*I;G1k@B&DN+jxt86gz0+cDSDOUe0Iw(T zQjV8n*&CdSj1-nD?o?7248~T>%r?PrpA!RW-9K67MB@3v0v8cTgUfY zhMo6LckFb<%&T5DZo>3`f41RsA?=7cjbDUtF4EN$MhI(W{6q9vIYW+red_byXLRKJ zI1K9?IL_`})R>K0DKap?I=G&jbunY$fDzA3W`Ry2viSD-KLc_2aV<%RFmt_a9Pc*$ z9WzBpVc~la!CFpc@Faot2@Xv7%-FuUn&Q#;*cTNni@WOjOnEYqhU$>589V#*nnjO1 z^nOKImB97`jRP_IzI-##!+cKwEb0^&7gKy(JjouC{%-3Z-@pAX#}9_%*M|F4XVHk? z?LvW7-H&`{4N@{F#F@- zAIl1vE+Cy*5wp*0Da`U%rDlyUkWZ?=w&h|iT{M)@so?+j{L?cZ{WZ2e7*IO{vP1lJ znw*@BWC{mmrDb%afKaYo`DDkB8w)f6|S*l1Yj2q0TbcO@^PmruWlJYW}C z*EqpVa!J^_u_^|zSPn73D!aCTs)C{ZX2A{^hL`rpgsU0F<pbCyWG7zsv zVUGb@-AsY(rWq)^W7aHjsi@Yq%TgZt@(;3O(qFe*yw;ZtU~p2-r~S4Cne24aoy9M% z|Lv~;a!(moD=-{xaI98=g@=qW`|~M zg&%T2Y|6WxYo-wIM))`g`M<>v<~{!EpMIM9Wc+V#lEQh|pD)X|E}yeO$qInSR)Jpt zKCyeT3~nNw)14HmFa^v^e9kTOK`8?<9{u$d=o-s3GgSCCu*0m-=~t9v%ueOdq;&8$ z9J0Q8C~8#uT_KvN^Clz5ah!p=@_X-6%+v4ffnQ`E8Lo-coew^C`){kB`kK$BoZ&aX zVupKz*CMhaz=Z`VZ(^eXk~}2{R1$=EI}7SSzAn%6*YfQV=IobZSrw?TiY;===#23z z<5v6Azpy8r8Xps$be<{YTFZ4q2oHdfppgc3Za1yJ{Wmo$zc5>|f)&Qql?%4ou(d-_ zW6SX#b&S7R$ge@R{VfUi;In|{#E+xHEw92oe*wr=0$>C4!(F-mOHVK;t91D7T%>yT z7k*_#A%H+lNKiN;{o~f!gnt!l;%xsVb*an@gKtx=D?)f6P;%e}*0wjtpSk@t^UpqR zLbkXH7I~n;GoEAh>}uiqp2A7n$(js$q2Ns+k$YU!6e`VF0L<^BV{PxFP6v?4+4R?E z4GAMy2iJ`nZXmbXeMx0zRwm=VT=;D&YcuUNymYQ2=C|_{gX*V6N5mfQNxJe3{hGh8 zJ3@FQyy`VV8*SFbHQxPK+p~W-(z*{w3KHamzmc-6=>l*$Ap*5~A7LPPlJ1yd%HUa! z!A=m1G9IJ5v0h$h`3kO)PpJ+eUk8%>WC$HI%emIVKBtSulh|zn3u!PjxPN9#a*-AW zoePN?$6SZ(uN>a5{_(?x`2E8oW5d@=oC@qo^X;g@d%TF$d7=#=h(i0ipdBHhWlQMfR{gYnUN5sh@;I7>#^Jc zyQczpo;4`(s&rbAiXRV#&x}|3R&-qL_goUulof)LfiMr!cpGFxN!3M(2O*~6@o zKkTb$MA-Q4s)z85lGO6xZc^zFShW|AiWA%SDKz3QLR3LIYg0IMKV+L=qkn$4@1lIC z2;l+qymVqe6|k^sdvyDpH)?)*cXs30U)~(DAT3fAi>QNpeME#jip?t2@Nd7c1_p&e z*rt3~Xpz9mxP4}KoQ}zvFnM9)VOifKWR^FYYlU1e6Jj8|zkG|i1%as!$OukEaJyvK z;~a_~oWa62DmQ#>HaOczqgAyBO*lEZuD0%UbaX65h9zT}ok8$QIIkmX;<5WH-#J1& zow`|Au!9mrYn#%_HvIDF(#I!u&Rab#{Eo0@Q#~jq@UB2CONzqBBMMjBJHtI~^EV@& z7nNFj*m*3B#sqbyF#FjaVEcyc7!C*UvkGWwysoj%e2%^_y!%aJfnvqe=}*^3V)Gwt z&s4`NfLT!6;U4<7nv?f9uXOyqAw&QglR07oVd@B7Q*`sMwiw@e^{8pvk9Hm0`6SQu zoS@N^9*ndhxboeruPzOr?T#2#BC=Yx&yC@~rUk+-Ad52{(vV5{930YNQjU z!^WNZPq%f(*8Nc#FW(v)8SjjWj1qRK`!<{5c>256@9z~Mobhpk* z0vEPnWRNxNgk)CJZSAB(hFz{x=7WbXYES>WQ`z*MNF}a}4MV~;o^{$`h$14&sW=`# zox8>F_wRoMA>OZOGubWqxdpSpKE$qMUF#v!XFuCq^X~Kl@wic%?(oT>mguxWnqlHl zDSmwi>n&mw3G^n`nmnAg632-^6JpO7mU!E8W=Uqq4Q5B@%%}7^McbZgQTg5$ zMcvMBYxnl7#4KloH42;4V38(}CB|=T{a-2nafAq(m{{B{1yIBer1JztSId$Wf0qt2 z{vA8dsjw0uLn{hRjMF4%=q?(EoVa*+K}Exf=qh7b_mrgBEf%|30`tS(&{d~%i0xg9 zAlU_@OJ8Mhb)A-kJL|;?(K*cgi(33hag;flEcjJ5W}DxqT(}x!|Hl!+gJq<6)JO&t z$5~ii4w8kKqQYWM%H*<>4vhg@uUcrr$kE3tUOBNceQNB}Cr#Tv!z@=#n>NjU@kE!c zqN0pO<&379(-%mAP0V7W69prcmstN_MuIoOL>@qRU%YYrpMwA-pv!e=%)#Q7>+a6u QVa%F-+q93SJpA+j4_IIKX8-^I literal 0 HcmV?d00001 diff --git a/SOL007_hackfest_basic_ns/Licenses/license.lic b/SOL007_hackfest_basic_ns/Licenses/license.lic new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/SOL007_hackfest_basic_ns/Licenses/license.lic @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/SOL007_hackfest_basic_ns/hackfest_basic_nsd.mf b/SOL007_hackfest_basic_ns/hackfest_basic_nsd.mf new file mode 100644 index 00000000..9fb7bd74 --- /dev/null +++ b/SOL007_hackfest_basic_ns/hackfest_basic_nsd.mf @@ -0,0 +1,12 @@ + +nsd_invariant_id: default-id +nsd_name: default-name +nsd_designer: OSM +nsd_file_structure_version: 1.0 +nsd_release_date_time: 2021-11-09T18:10:30.117516-03:00 +compatible_specification_versions: 3.3.1 + +Source: hackfest_basic_nsd.yaml +Algorithm: SHA-512 +Hash: 7c46a2da0331a99685e5097a9e5c370dfabfebb4730989582da484f1f98783e6442ba9a8c56b5fb3f5195463d81b6a6a90caacc20eb710b1f4f12c65aac74d33 + diff --git a/SOL007_hackfest_basic_ns/hackfest_basic_nsd.yaml b/SOL007_hackfest_basic_ns/hackfest_basic_nsd.yaml new file mode 100644 index 00000000..be76b264 --- /dev/null +++ b/SOL007_hackfest_basic_ns/hackfest_basic_nsd.yaml @@ -0,0 +1,21 @@ +nsd: + nsd: + - description: Simple NS with a single VNF and a single VL + df: + - id: default-df + vnf-profile: + - id: '1' + virtual-link-connectivity: + - constituent-cpd-id: + - constituent-base-element-id: '1' + constituent-cpd-id: vnf-cp0-ext + virtual-link-profile-id: mgmtnet + vnfd-id: hackfest_basic-vnf + id: hackfest_basic-ns + name: hackfest_basic-ns + version: '1.0' + virtual-link-desc: + - id: mgmtnet + mgmt-network: true + vnfd-id: + - hackfest_basic-vnf diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/ChangeLog.txt b/charm-packages/SOL004_k8s_proxy_charm_vnf/ChangeLog.txt new file mode 100644 index 00000000..8f45952f --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/ChangeLog.txt @@ -0,0 +1,5 @@ + +1.0.0 + +- Package converted with OSM package migration tool. + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Files/icons/osm.png b/charm-packages/SOL004_k8s_proxy_charm_vnf/Files/icons/osm.png new file mode 100644 index 0000000000000000000000000000000000000000..62012d2a2b491bdcd536d62c3c3c863c0d8c1b33 GIT binary patch literal 55888 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 QVa%F-+q93SJpA+j4_IIKX8-^I literal 0 HcmV?d00001 diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Licenses/license.lic b/charm-packages/SOL004_k8s_proxy_charm_vnf/Licenses/license.lic new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Licenses/license.lic @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/actions.yaml b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/actions.yaml new file mode 100644 index 00000000..f9882c47 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/actions.yaml @@ -0,0 +1,54 @@ +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +touch: + description: "Touch a file on the VNF." + params: + filename: + description: "The name of the file to touch." + type: string + default: "" + required: + - filename + +# Standard OSM functions +start: + description: "Stop the service on the VNF." +stop: + description: "Stop the service on the VNF." +restart: + description: "Stop the service on the VNF." +reboot: + description: "Reboot the VNF virtual machine." +upgrade: + description: "Upgrade the software on the VNF." + +# Required by charms.osm.sshproxy +run: + description: "Run an arbitrary command" + params: + command: + description: "The command to execute." + type: string + default: "" + required: + - command +generate-ssh-key: + description: "Generate a new SSH keypair for this unit. This will replace any existing previously generated keypair." +verify-ssh-credentials: + description: "Verify that this unit can authenticate with server specified by ssh-hostname and ssh-username." +get-ssh-public-key: + description: "Get the public SSH key for this unit." diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/config.yaml b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/config.yaml new file mode 100644 index 00000000..93e3cab0 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/config.yaml @@ -0,0 +1,41 @@ +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## +options: + ssh-hostname: + type: string + default: "" + description: "The hostname or IP address of the machine to" + ssh-username: + type: string + default: "" + description: "The username to login as." + ssh-password: + type: string + default: "" + description: "The password used to authenticate." + ssh-public-key: + type: string + default: "" + description: "The public key of this unit." + ssh-key-type: + type: string + default: "rsa" + description: "The type of encryption to use for the SSH key." + ssh-key-bits: + type: int + default: 4096 + description: "The number of bits to use for the SSH key." diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/install b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/install new file mode 100755 index 00000000..e23b12b7 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/install @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +import sys + +sys.path.append("lib") + +from charms.osm.sshproxy import SSHProxyCharm +from ops.main import main + +class MySSHProxyCharm(SSHProxyCharm): + + def __init__(self, framework, key): + super().__init__(framework, key) + + # Listen to charm events + self.framework.observe(self.on.config_changed, self.on_config_changed) + self.framework.observe(self.on.install, self.on_install) + self.framework.observe(self.on.start, self.on_start) + + # Listen to the touch action event + self.framework.observe(self.on.touch_action, self.on_touch_action) + + def on_config_changed(self, event): + """Handle changes in configuration""" + super().on_config_changed(event) + + def on_install(self, event): + """Called when the charm is being installed""" + super().on_install(event) + + def on_start(self, event): + """Called when the charm is being started""" + super().on_start(event) + + def on_touch_action(self, event): + """Touch a file.""" + + if self.model.unit.is_leader(): + filename = event.params["filename"] + proxy = self.get_ssh_proxy() + stdout, stderr = proxy.run("touch {}".format(filename)) + event.set_results({"output": stdout}) + else: + event.fail("Unit is not leader") + return + +if __name__ == "__main__": + main(MySSHProxyCharm) + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/start b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/start new file mode 100755 index 00000000..e23b12b7 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/start @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +import sys + +sys.path.append("lib") + +from charms.osm.sshproxy import SSHProxyCharm +from ops.main import main + +class MySSHProxyCharm(SSHProxyCharm): + + def __init__(self, framework, key): + super().__init__(framework, key) + + # Listen to charm events + self.framework.observe(self.on.config_changed, self.on_config_changed) + self.framework.observe(self.on.install, self.on_install) + self.framework.observe(self.on.start, self.on_start) + + # Listen to the touch action event + self.framework.observe(self.on.touch_action, self.on_touch_action) + + def on_config_changed(self, event): + """Handle changes in configuration""" + super().on_config_changed(event) + + def on_install(self, event): + """Called when the charm is being installed""" + super().on_install(event) + + def on_start(self, event): + """Called when the charm is being started""" + super().on_start(event) + + def on_touch_action(self, event): + """Touch a file.""" + + if self.model.unit.is_leader(): + filename = event.params["filename"] + proxy = self.get_ssh_proxy() + stdout, stderr = proxy.run("touch {}".format(filename)) + event.set_results({"output": stdout}) + else: + event.fail("Unit is not leader") + return + +if __name__ == "__main__": + main(MySSHProxyCharm) + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/upgrade-charm b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/upgrade-charm new file mode 100755 index 00000000..e23b12b7 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/hooks/upgrade-charm @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +import sys + +sys.path.append("lib") + +from charms.osm.sshproxy import SSHProxyCharm +from ops.main import main + +class MySSHProxyCharm(SSHProxyCharm): + + def __init__(self, framework, key): + super().__init__(framework, key) + + # Listen to charm events + self.framework.observe(self.on.config_changed, self.on_config_changed) + self.framework.observe(self.on.install, self.on_install) + self.framework.observe(self.on.start, self.on_start) + + # Listen to the touch action event + self.framework.observe(self.on.touch_action, self.on_touch_action) + + def on_config_changed(self, event): + """Handle changes in configuration""" + super().on_config_changed(event) + + def on_install(self, event): + """Called when the charm is being installed""" + super().on_install(event) + + def on_start(self, event): + """Called when the charm is being started""" + super().on_start(event) + + def on_touch_action(self, event): + """Touch a file.""" + + if self.model.unit.is_leader(): + filename = event.params["filename"] + proxy = self.get_ssh_proxy() + stdout, stderr = proxy.run("touch {}".format(filename)) + event.set_results({"output": stdout}) + else: + event.fail("Unit is not leader") + return + +if __name__ == "__main__": + main(MySSHProxyCharm) + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/libansible.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/libansible.py new file mode 100644 index 00000000..32fd26ae --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/libansible.py @@ -0,0 +1,108 @@ +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +import fnmatch +import os +import yaml +import subprocess +import sys + +sys.path.append("lib") +import charmhelpers.fetch + + +ansible_hosts_path = "/etc/ansible/hosts" + + +def install_ansible_support(from_ppa=True, ppa_location="ppa:ansible/ansible"): + """Installs the ansible package. + + By default it is installed from the `PPA`_ linked from + the ansible `website`_ or from a ppa specified by a charm config.. + + .. _PPA: https://launchpad.net/~rquillo/+archive/ansible + .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu + + If from_ppa is empty, you must ensure that the package is available + from a configured repository. + """ + if from_ppa: + charmhelpers.fetch.add_source(ppa_location) + charmhelpers.fetch.apt_update(fatal=True) + charmhelpers.fetch.apt_install("ansible") + with open(ansible_hosts_path, "w+") as hosts_file: + hosts_file.write("localhost ansible_connection=local") + + +def create_hosts(hostname, username, password, hosts): + inventory_path = "/etc/ansible/hosts" + + with open(inventory_path, "w") as f: + f.write("[{}]\n".format(hosts)) + h1 = "host ansible_host={0} ansible_user={1} ansible_password={2}\n".format( + hostname, username, password + ) + f.write(h1) + + +def create_ansible_cfg(): + ansible_config_path = "/etc/ansible/ansible.cfg" + + with open(ansible_config_path, "w") as f: + f.write("[defaults]\n") + f.write("host_key_checking = False\n") + + +# Function to find the playbook path +def find(pattern, path): + result = "" + for root, dirs, files in os.walk(path): + for name in files: + if fnmatch.fnmatch(name, pattern): + result = os.path.join(root, name) + return result + + +def execute_playbook(playbook_file, hostname, user, password, vars_dict=None): + playbook_path = find(playbook_file, "/var/lib/juju/agents/") + + with open(playbook_path, "r") as f: + playbook_data = yaml.load(f) + + hosts = "all" + if "hosts" in playbook_data[0].keys() and playbook_data[0]["hosts"]: + hosts = playbook_data[0]["hosts"] + + create_ansible_cfg() + create_hosts(hostname, user, password, hosts) + + call = "ansible-playbook {} ".format(playbook_path) + + if vars_dict and isinstance(vars_dict, dict) and len(vars_dict) > 0: + call += "--extra-vars " + + string_var = "" + for k,v in vars_dict.items(): + string_var += "{}={} ".format(k, v) + + string_var = string_var.strip() + call += '"{}"'.format(string_var) + + call = call.strip() + result = subprocess.check_output(call, shell=True) + + return result diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/ns.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/ns.py new file mode 100644 index 00000000..25be4056 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/ns.py @@ -0,0 +1,301 @@ +# A prototype of a library to aid in the development and operation of +# OSM Network Service charms + +import asyncio +import logging +import os +import os.path +import re +import subprocess +import sys +import time +import yaml + +try: + import juju +except ImportError: + # Not all cloud images are created equal + if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): + # Update the apt cache + subprocess.check_call(["apt-get", "update"]) + + # Install the Python3 package + subprocess.check_call(["apt-get", "install", "-y", "python3", "python3-pip"],) + + + # Install the libjuju build dependencies + subprocess.check_call(["apt-get", "install", "-y", "libffi-dev", "libssl-dev"],) + + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "juju"], + ) + +from juju.controller import Controller + +# Quiet the debug logging +logging.getLogger('websockets.protocol').setLevel(logging.INFO) +logging.getLogger('juju.client.connection').setLevel(logging.WARN) +logging.getLogger('juju.model').setLevel(logging.WARN) +logging.getLogger('juju.machine').setLevel(logging.WARN) + + +class NetworkService: + """A lightweight interface to the Juju controller. + + This NetworkService client is specifically designed to allow a higher-level + "NS" charm to interoperate with "VNF" charms, allowing for the execution of + Primitives across other charms within the same model. + """ + endpoint = None + user = 'admin' + secret = None + port = 17070 + loop = None + client = None + model = None + cacert = None + + def __init__(self, user, secret, endpoint=None): + + self.user = user + self.secret = secret + if endpoint is None: + addresses = os.environ['JUJU_API_ADDRESSES'] + for address in addresses.split(' '): + self.endpoint = address + else: + self.endpoint = endpoint + + # Stash the name of the model + self.model = os.environ['JUJU_MODEL_NAME'] + + # Load the ca-cert from agent.conf + AGENT_PATH = os.path.dirname(os.environ['JUJU_CHARM_DIR']) + with open("{}/agent.conf".format(AGENT_PATH), "r") as f: + try: + y = yaml.safe_load(f) + self.cacert = y['cacert'] + except yaml.YAMLError as exc: + print("Unable to find Juju ca-cert.") + raise exc + + # Create our event loop + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + async def connect(self): + """Connect to the Juju controller.""" + controller = Controller() + + print( + "Connecting to controller... ws://{}:{} as {}/{}".format( + self.endpoint, + self.port, + self.user, + self.secret[-4:].rjust(len(self.secret), "*"), + ) + ) + await controller.connect( + endpoint=self.endpoint, + username=self.user, + password=self.secret, + cacert=self.cacert, + ) + + return controller + + def __del__(self): + self.logout() + + async def disconnect(self): + """Disconnect from the Juju controller.""" + if self.client: + print("Disconnecting Juju controller") + await self.client.disconnect() + + def login(self): + """Login to the Juju controller.""" + if not self.client: + # Connect to the Juju API server + self.client = self.loop.run_until_complete(self.connect()) + return self.client + + def logout(self): + """Logout of the Juju controller.""" + + if self.loop: + print("Disconnecting from API") + self.loop.run_until_complete(self.disconnect()) + + def FormatApplicationName(self, *args): + """ + Generate a Juju-compatible Application name + + :param args tuple: Positional arguments to be used to construct the + application name. + + Limitations:: + - Only accepts characters a-z and non-consequitive dashes (-) + - Application name should not exceed 50 characters + + Examples:: + + FormatApplicationName("ping_pong_ns", "ping_vnf", "a") + """ + appname = "" + for c in "-".join(list(args)): + if c.isdigit(): + c = chr(97 + int(c)) + elif not c.isalpha(): + c = "-" + appname += c + + return re.sub('-+', '-', appname.lower()) + + def GetApplicationName(self, nsr_name, vnf_name, vnf_member_index): + """Get the runtime application name of a VNF/VDU. + + This will generate an application name matching the name of the deployed charm, + given the right parameters. + + :param nsr_name str: The name of the running Network Service, as specified at instantiation. + :param vnf_name str: The name of the VNF or VDU + :param vnf_member_index: The vnf-member-index as specified in the descriptor + """ + + application_name = self.FormatApplicationName(nsr_name, vnf_member_index, vnf_name) + + # This matches the logic used by the LCM + application_name = application_name[0:48] + vca_index = int(vnf_member_index) - 1 + application_name += '-' + chr(97 + vca_index // 26) + chr(97 + vca_index % 26) + + return application_name + + def ExecutePrimitiveGetOutput(self, application, primitive, params={}, timeout=600): + """Execute a single primitive and return it's output. + + This is a blocking method that will execute a single primitive and wait + for its completion before return it's output. + + :param application str: The application name provided by `GetApplicationName`. + :param primitive str: The name of the primitive to execute. + :param params list: A list of parameters. + :param timeout int: A timeout, in seconds, to wait for the primitive to finish. Defaults to 600 seconds. + """ + uuid = self.ExecutePrimitive(application, primitive, params) + + status = None + output = None + + starttime = time.time() + while(time.time() < starttime + timeout): + status = self.GetPrimitiveStatus(uuid) + if status in ['completed', 'failed']: + break + time.sleep(10) + + # When the primitive is done, get the output + if status in ['completed', 'failed']: + output = self.GetPrimitiveOutput(uuid) + + return output + + def ExecutePrimitive(self, application, primitive, params={}): + """Execute a primitive. + + This is a non-blocking method to execute a primitive. It will return + the UUID of the queued primitive execution, which you can use + for subsequent calls to `GetPrimitiveStatus` and `GetPrimitiveOutput`. + + :param application string: The name of the application + :param primitive string: The name of the Primitive. + :param params list: A list of parameters. + + :returns uuid string: The UUID of the executed Primitive + """ + uuid = None + + if not self.client: + self.login() + + model = self.loop.run_until_complete( + self.client.get_model(self.model) + ) + + # Get the application + if application in model.applications: + app = model.applications[application] + + # Execute the primitive + unit = app.units[0] + if unit: + action = self.loop.run_until_complete( + unit.run_action(primitive, **params) + ) + uuid = action.id + print("Executing action: {}".format(uuid)) + self.loop.run_until_complete( + model.disconnect() + ) + else: + # Invalid mapping: application not found. Raise exception + raise Exception("Application not found: {}".format(application)) + + return uuid + + def GetPrimitiveStatus(self, uuid): + """Get the status of a Primitive execution. + + This will return one of the following strings: + - pending + - running + - completed + - failed + + :param uuid string: The UUID of the executed Primitive. + :returns: The status of the executed Primitive + """ + status = None + + if not self.client: + self.login() + + model = self.loop.run_until_complete( + self.client.get_model(self.model) + ) + + status = self.loop.run_until_complete( + model.get_action_status(uuid) + ) + + self.loop.run_until_complete( + model.disconnect() + ) + + return status[uuid] + + def GetPrimitiveOutput(self, uuid): + """Get the output of a completed Primitive execution. + + + :param uuid string: The UUID of the executed Primitive. + :returns: The output of the execution, or None if it's still running. + """ + result = None + if not self.client: + self.login() + + model = self.loop.run_until_complete( + self.client.get_model(self.model) + ) + + result = self.loop.run_until_complete( + model.get_action_output(uuid) + ) + + self.loop.run_until_complete( + model.disconnect() + ) + + return result diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/proxy_cluster.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/proxy_cluster.py new file mode 100644 index 00000000..f323a3af --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/proxy_cluster.py @@ -0,0 +1,59 @@ +import socket + +from ops.framework import Object, StoredState + + +class ProxyCluster(Object): + + state = StoredState() + + def __init__(self, charm, relation_name): + super().__init__(charm, relation_name) + self._relation_name = relation_name + self._relation = self.framework.model.get_relation(self._relation_name) + + self.framework.observe(charm.on.ssh_keys_initialized, self.on_ssh_keys_initialized) + + self.state.set_default(ssh_public_key=None) + self.state.set_default(ssh_private_key=None) + + def on_ssh_keys_initialized(self, event): + if not self.framework.model.unit.is_leader(): + raise RuntimeError("The initial unit of a cluster must also be a leader.") + + self.state.ssh_public_key = event.ssh_public_key + self.state.ssh_private_key = event.ssh_private_key + if not self.is_joined: + event.defer() + return + + self._relation.data[self.model.app][ + "ssh_public_key" + ] = self.state.ssh_public_key + self._relation.data[self.model.app][ + "ssh_private_key" + ] = self.state.ssh_private_key + + @property + def is_joined(self): + return self._relation is not None + + @property + def ssh_public_key(self): + if self.is_joined: + return self._relation.data[self.model.app].get("ssh_public_key") + + @property + def ssh_private_key(self): + if self.is_joined: + return self._relation.data[self.model.app].get("ssh_private_key") + + @property + def is_cluster_initialized(self): + return ( + True + if self.is_joined + and self._relation.data[self.model.app].get("ssh_public_key") + and self._relation.data[self.model.app].get("ssh_private_key") + else False + ) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/sshproxy.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/sshproxy.py new file mode 100644 index 00000000..e2c311e5 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/charms/osm/sshproxy.py @@ -0,0 +1,375 @@ +"""Module to help with executing commands over SSH.""" +## +# Copyright 2016 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +# from charmhelpers.core import unitdata +# from charmhelpers.core.hookenv import log + +import io +import ipaddress +import subprocess +import os +import socket +import shlex +import traceback +import sys + +from subprocess import ( + check_call, + Popen, + CalledProcessError, + PIPE, +) + +from ops.charm import CharmBase, CharmEvents +from ops.framework import StoredState, EventBase, EventSource +from ops.main import main +from ops.model import ( + ActiveStatus, + BlockedStatus, + MaintenanceStatus, + WaitingStatus, + ModelError, +) +import os +import subprocess +from .proxy_cluster import ProxyCluster + +import logging + + +logger = logging.getLogger(__name__) + +class SSHKeysInitialized(EventBase): + def __init__(self, handle, ssh_public_key, ssh_private_key): + super().__init__(handle) + self.ssh_public_key = ssh_public_key + self.ssh_private_key = ssh_private_key + + def snapshot(self): + return { + "ssh_public_key": self.ssh_public_key, + "ssh_private_key": self.ssh_private_key, + } + + def restore(self, snapshot): + self.ssh_public_key = snapshot["ssh_public_key"] + self.ssh_private_key = snapshot["ssh_private_key"] + + +class ProxyClusterEvents(CharmEvents): + ssh_keys_initialized = EventSource(SSHKeysInitialized) + + +class SSHProxyCharm(CharmBase): + + state = StoredState() + on = ProxyClusterEvents() + + def __init__(self, framework, key): + super().__init__(framework, key) + + self.peers = ProxyCluster(self, "proxypeer") + + # SSH Proxy actions (primitives) + self.framework.observe(self.on.generate_ssh_key_action, self.on_generate_ssh_key_action) + self.framework.observe(self.on.get_ssh_public_key_action, self.on_get_ssh_public_key_action) + self.framework.observe(self.on.run_action, self.on_run_action) + self.framework.observe(self.on.verify_ssh_credentials_action, self.on_verify_ssh_credentials_action) + + self.framework.observe(self.on.proxypeer_relation_changed, self.on_proxypeer_relation_changed) + + def get_ssh_proxy(self): + """Get the SSHProxy instance""" + proxy = SSHProxy( + hostname=self.model.config["ssh-hostname"], + username=self.model.config["ssh-username"], + password=self.model.config["ssh-password"], + ) + return proxy + + def on_proxypeer_relation_changed(self, event): + if self.peers.is_cluster_initialized and not SSHProxy.has_ssh_key(): + pubkey = self.peers.ssh_public_key + privkey = self.peers.ssh_private_key + SSHProxy.write_ssh_keys(public=pubkey, private=privkey) + self.verify_credentials() + else: + event.defer() + + def on_config_changed(self, event): + """Handle changes in configuration""" + self.verify_credentials() + + def on_install(self, event): + SSHProxy.install() + + def on_start(self, event): + """Called when the charm is being installed""" + if not self.peers.is_joined: + event.defer() + return + + unit = self.model.unit + + if not SSHProxy.has_ssh_key(): + unit.status = MaintenanceStatus("Generating SSH keys...") + pubkey = None + privkey = None + if self.model.unit.is_leader(): + if self.peers.is_cluster_initialized: + SSHProxy.write_ssh_keys( + public=self.peers.ssh_public_key, + private=self.peers.ssh_private_key, + ) + else: + SSHProxy.generate_ssh_key() + self.on.ssh_keys_initialized.emit( + SSHProxy.get_ssh_public_key(), SSHProxy.get_ssh_private_key() + ) + self.verify_credentials() + + def verify_credentials(self): + unit = self.model.unit + + # Unit should go into a waiting state until verify_ssh_credentials is successful + unit.status = WaitingStatus("Waiting for SSH credentials") + proxy = self.get_ssh_proxy() + verified, _ = proxy.verify_credentials() + if verified: + unit.status = ActiveStatus() + else: + unit.status = BlockedStatus("Invalid SSH credentials.") + return verified + + ##################### + # SSH Proxy methods # + ##################### + def on_generate_ssh_key_action(self, event): + """Generate a new SSH keypair for this unit.""" + if self.model.unit.is_leader(): + if not SSHProxy.generate_ssh_key(): + event.fail("Unable to generate ssh key") + else: + event.fail("Unit is not leader") + return + + def on_get_ssh_public_key_action(self, event): + """Get the SSH public key for this unit.""" + if self.model.unit.is_leader(): + pubkey = SSHProxy.get_ssh_public_key() + event.set_results({"pubkey": SSHProxy.get_ssh_public_key()}) + else: + event.fail("Unit is not leader") + return + + def on_run_action(self, event): + """Run an arbitrary command on the remote host.""" + if self.model.unit.is_leader(): + cmd = event.params["command"] + proxy = self.get_ssh_proxy() + stdout, stderr = proxy.run(cmd) + event.set_results({"output": stdout}) + if len(stderr): + event.fail(stderr) + else: + event.fail("Unit is not leader") + return + + def on_verify_ssh_credentials_action(self, event): + """Verify the SSH credentials for this unit.""" + unit = self.model.unit + if unit.is_leader(): + proxy = self.get_ssh_proxy() + verified, stderr = proxy.verify_credentials() + if verified: + event.set_results({"verified": True}) + unit.status = ActiveStatus() + else: + event.set_results({"verified": False, "stderr": stderr}) + event.fail("Not verified") + unit.status = BlockedStatus("Invalid SSH credentials.") + + else: + event.fail("Unit is not leader") + return + + +class LeadershipError(ModelError): + def __init__(self): + super().__init__("not leader") + +class SSHProxy: + private_key_path = "/root/.ssh/id_sshproxy" + public_key_path = "/root/.ssh/id_sshproxy.pub" + key_type = "rsa" + key_bits = 4096 + + def __init__(self, hostname: str, username: str, password: str = ""): + self.hostname = hostname + self.username = username + self.password = password + + @staticmethod + def install(): + check_call("apt update && apt install -y openssh-client sshpass", shell=True) + + @staticmethod + def generate_ssh_key(): + """Generate a 4096-bit rsa keypair.""" + if not os.path.exists(SSHProxy.private_key_path): + cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( + SSHProxy.key_type, SSHProxy.key_bits, SSHProxy.private_key_path, + ) + + try: + check_call(cmd, shell=True) + except CalledProcessError: + return False + + return True + + @staticmethod + def write_ssh_keys(public, private): + """Write a 4096-bit rsa keypair.""" + with open(SSHProxy.public_key_path, "w") as f: + f.write(public) + f.close() + with open(SSHProxy.private_key_path, "w") as f: + f.write(private) + f.close() + + @staticmethod + def get_ssh_public_key(): + publickey = "" + if os.path.exists(SSHProxy.private_key_path): + with open(SSHProxy.public_key_path, "r") as f: + publickey = f.read() + return publickey + + @staticmethod + def get_ssh_private_key(): + privatekey = "" + if os.path.exists(SSHProxy.private_key_path): + with open(SSHProxy.private_key_path, "r") as f: + privatekey = f.read() + return privatekey + + @staticmethod + def has_ssh_key(): + return True if os.path.exists(SSHProxy.private_key_path) else False + + def run(self, cmd: str) -> (str, str): + """Run a command remotely via SSH. + + Note: The previous behavior was to run the command locally if SSH wasn't + configured, but that can lead to cases where execution succeeds when you'd + expect it not to. + """ + if isinstance(cmd, str): + cmd = shlex.split(cmd) + + host = self._get_hostname() + user = self.username + passwd = self.password + key = self.private_key_path + + # Make sure we have everything we need to connect + if host and user: + return self.ssh(cmd) + + raise Exception("Invalid SSH credentials.") + + def scp(self, source_file, destination_file): + """Execute an scp command. Requires a fully qualified source and + destination. + + :param str source_file: Path to the source file + :param str destination_file: Path to the destination file + :raises: :class:`CalledProcessError` if the command fails + """ + cmd = [ + "sshpass", + "-p", + self.password, + "scp", + "-i", + os.path.expanduser(self.private_key_path), + "-o", + "StrictHostKeyChecking=no", + "-q", + "-B", + ] + destination = "{}@{}:{}".format(self.username, self.hostname, destination_file) + cmd.extend([source_file, destination]) + subprocess.run(cmd, check=True) + + def ssh(self, command): + """Run a command remotely via SSH. + + :param list(str) command: The command to execute + :return: tuple: The stdout and stderr of the command execution + :raises: :class:`CalledProcessError` if the command fails + """ + + destination = "{}@{}".format(self.username, self.hostname) + cmd = [ + "sshpass", + "-p", + self.password, + "ssh", + "-i", + os.path.expanduser(self.private_key_path), + "-o", + "StrictHostKeyChecking=no", + "-q", + destination, + ] + cmd.extend(command) + output = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return (output.stdout.decode("utf-8").strip(), output.stderr.decode("utf-8").strip()) + + def verify_credentials(self): + """Verify the SSH credentials. + + :return (bool, str): Verified, Stderr + """ + verified = False + try: + (stdout, stderr) = self.run("hostname") + verified = True + except CalledProcessError as e: + stderr = "Command failed: {} ({})".format(" ".join(e.cmd), str(e.output)) + except (TimeoutError, socket.timeout): + stderr = "Timeout attempting to reach {}".format(self._get_hostname()) + except Exception as error: + tb = traceback.format_exc() + stderr = "Unhandled exception: {}".format(tb) + return verified, stderr + + ################### + # Private methods # + ################### + def _get_hostname(self): + """Get the hostname for the ssh target. + + HACK: This function was added to work around an issue where the + ssh-hostname was passed in the format of a.b.c.d;a.b.c.d, where the first + is the floating ip, and the second the non-floating ip, for an Openstack + instance. + """ + return self.hostname.split(";")[0] diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/__init__.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/__init__.py new file mode 100644 index 00000000..f17b2969 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The Operator Framework.""" + +from .version import version as __version__ # noqa: F401 (imported but unused) + +# Import here the bare minimum to break the circular import between modules +from . import charm # noqa: F401 (imported but unused) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/charm.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/charm.py new file mode 100755 index 00000000..d898de85 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/charm.py @@ -0,0 +1,575 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum +import os +import pathlib +import typing + +import yaml + +from ops.framework import Object, EventSource, EventBase, Framework, ObjectEvents +from ops import model + + +def _loadYaml(source): + if yaml.__with_libyaml__: + return yaml.load(source, Loader=yaml.CSafeLoader) + return yaml.load(source, Loader=yaml.SafeLoader) + + +class HookEvent(EventBase): + """A base class for events that trigger because of a Juju hook firing.""" + + +class ActionEvent(EventBase): + """A base class for events that trigger when a user asks for an Action to be run. + + To read the parameters for the action, see the instance variable `params`. + To respond with the result of the action, call `set_results`. To add progress + messages that are visible as the action is progressing use `log`. + + :ivar params: The parameters passed to the action (read by action-get) + """ + + def defer(self): + """Action events are not deferable like other events. + + This is because an action runs synchronously and the user is waiting for the result. + """ + raise RuntimeError('cannot defer action events') + + def restore(self, snapshot: dict) -> None: + """Used by the operator framework to record the action. + + Not meant to be called directly by Charm code. + """ + env_action_name = os.environ.get('JUJU_ACTION_NAME') + event_action_name = self.handle.kind[:-len('_action')].replace('_', '-') + if event_action_name != env_action_name: + # This could only happen if the dev manually emits the action, or from a bug. + raise RuntimeError('action event kind does not match current action') + # Params are loaded at restore rather than __init__ because + # the model is not available in __init__. + self.params = self.framework.model._backend.action_get() + + def set_results(self, results: typing.Mapping) -> None: + """Report the result of the action. + + Args: + results: The result of the action as a Dict + """ + self.framework.model._backend.action_set(results) + + def log(self, message: str) -> None: + """Send a message that a user will see while the action is running. + + Args: + message: The message for the user. + """ + self.framework.model._backend.action_log(message) + + def fail(self, message: str = '') -> None: + """Report that this action has failed. + + Args: + message: Optional message to record why it has failed. + """ + self.framework.model._backend.action_fail(message) + + +class InstallEvent(HookEvent): + """Represents the `install` hook from Juju.""" + + +class StartEvent(HookEvent): + """Represents the `start` hook from Juju.""" + + +class StopEvent(HookEvent): + """Represents the `stop` hook from Juju.""" + + +class RemoveEvent(HookEvent): + """Represents the `remove` hook from Juju. """ + + +class ConfigChangedEvent(HookEvent): + """Represents the `config-changed` hook from Juju.""" + + +class UpdateStatusEvent(HookEvent): + """Represents the `update-status` hook from Juju.""" + + +class UpgradeCharmEvent(HookEvent): + """Represents the `upgrade-charm` hook from Juju. + + This will be triggered when a user has run `juju upgrade-charm`. It is run after Juju + has unpacked the upgraded charm code, and so this event will be handled with new code. + """ + + +class PreSeriesUpgradeEvent(HookEvent): + """Represents the `pre-series-upgrade` hook from Juju. + + This happens when a user has run `juju upgrade-series MACHINE prepare` and + will fire for each unit that is running on the machine, telling them that + the user is preparing to upgrade the Machine's series (eg trusty->bionic). + The charm should take actions to prepare for the upgrade (a database charm + would want to write out a version-independent dump of the database, so that + when a new version of the database is available in a new series, it can be + used.) + Once all units on a machine have run `pre-series-upgrade`, the user will + initiate the steps to actually upgrade the machine (eg `do-release-upgrade`). + When the upgrade has been completed, the :class:`PostSeriesUpgradeEvent` will fire. + """ + + +class PostSeriesUpgradeEvent(HookEvent): + """Represents the `post-series-upgrade` hook from Juju. + + This is run after the user has done a distribution upgrade (or rolled back + and kept the same series). It is called in response to + `juju upgrade-series MACHINE complete`. Charms are expected to do whatever + steps are necessary to reconfigure their applications for the new series. + """ + + +class LeaderElectedEvent(HookEvent): + """Represents the `leader-elected` hook from Juju. + + Juju will trigger this when a new lead unit is chosen for a given application. + This represents the leader of the charm information (not necessarily the primary + of a running application). The main utility is that charm authors can know + that only one unit will be a leader at any given time, so they can do + configuration, etc, that would otherwise require coordination between units. + (eg, selecting a password for a new relation) + """ + + +class LeaderSettingsChangedEvent(HookEvent): + """Represents the `leader-settings-changed` hook from Juju. + + Deprecated. This represents when a lead unit would call `leader-set` to inform + the other units of an application that they have new information to handle. + This has been deprecated in favor of using a Peer relation, and having the + leader set a value in the Application data bag for that peer relation. + (see :class:`RelationChangedEvent`). + """ + + +class CollectMetricsEvent(HookEvent): + """Represents the `collect-metrics` hook from Juju. + + Note that events firing during a CollectMetricsEvent are currently + sandboxed in how they can interact with Juju. To report metrics + use :meth:`.add_metrics`. + """ + + def add_metrics(self, metrics: typing.Mapping, labels: typing.Mapping = None) -> None: + """Record metrics that have been gathered by the charm for this unit. + + Args: + metrics: A collection of {key: float} pairs that contains the + metrics that have been gathered + labels: {key:value} strings that can be applied to the + metrics that are being gathered + """ + self.framework.model._backend.add_metrics(metrics, labels) + + +class RelationEvent(HookEvent): + """A base class representing the various relation lifecycle events. + + Charmers should not be creating RelationEvents directly. The events will be + generated by the framework from Juju related events. Users can observe them + from the various `CharmBase.on[relation_name].relation_*` events. + + Attributes: + relation: The Relation involved in this event + app: The remote application that has triggered this event + unit: The remote unit that has triggered this event. This may be None + if the relation event was triggered as an Application level event + """ + + def __init__(self, handle, relation, app=None, unit=None): + super().__init__(handle) + + if unit is not None and unit.app != app: + raise RuntimeError( + 'cannot create RelationEvent with application {} and unit {}'.format(app, unit)) + + self.relation = relation + self.app = app + self.unit = unit + + def snapshot(self) -> dict: + """Used by the framework to serialize the event to disk. + + Not meant to be called by Charm code. + """ + snapshot = { + 'relation_name': self.relation.name, + 'relation_id': self.relation.id, + } + if self.app: + snapshot['app_name'] = self.app.name + if self.unit: + snapshot['unit_name'] = self.unit.name + return snapshot + + def restore(self, snapshot: dict) -> None: + """Used by the framework to deserialize the event from disk. + + Not meant to be called by Charm code. + """ + self.relation = self.framework.model.get_relation( + snapshot['relation_name'], snapshot['relation_id']) + + app_name = snapshot.get('app_name') + if app_name: + self.app = self.framework.model.get_app(app_name) + else: + self.app = None + + unit_name = snapshot.get('unit_name') + if unit_name: + self.unit = self.framework.model.get_unit(unit_name) + else: + self.unit = None + + +class RelationCreatedEvent(RelationEvent): + """Represents the `relation-created` hook from Juju. + + This is triggered when a new relation to another app is added in Juju. This + can occur before units for those applications have started. All existing + relations should be established before start. + """ + + +class RelationJoinedEvent(RelationEvent): + """Represents the `relation-joined` hook from Juju. + + This is triggered whenever a new unit of a related application joins the relation. + (eg, a unit was added to an existing related app, or a new relation was established + with an application that already had units.) + """ + + +class RelationChangedEvent(RelationEvent): + """Represents the `relation-changed` hook from Juju. + + This is triggered whenever there is a change to the data bucket for a related + application or unit. Look at `event.relation.data[event.unit/app]` to see the + new information. + """ + + +class RelationDepartedEvent(RelationEvent): + """Represents the `relation-departed` hook from Juju. + + This is the inverse of the RelationJoinedEvent, representing when a unit + is leaving the relation (the unit is being removed, the app is being removed, + the relation is being removed). It is fired once for each unit that is + going away. + """ + + +class RelationBrokenEvent(RelationEvent): + """Represents the `relation-broken` hook from Juju. + + If a relation is being removed (`juju remove-relation` or `juju remove-application`), + once all the units have been removed, RelationBrokenEvent will fire to signal + that the relationship has been fully terminated. + """ + + +class StorageEvent(HookEvent): + """Base class representing Storage related events.""" + + +class StorageAttachedEvent(StorageEvent): + """Represents the `storage-attached` hook from Juju. + + Called when new storage is available for the charm to use. + """ + + +class StorageDetachingEvent(StorageEvent): + """Represents the `storage-detaching` hook from Juju. + + Called when storage a charm has been using is going away. + """ + + +class CharmEvents(ObjectEvents): + """The events that are generated by Juju in response to the lifecycle of an application.""" + + install = EventSource(InstallEvent) + start = EventSource(StartEvent) + stop = EventSource(StopEvent) + remove = EventSource(RemoveEvent) + update_status = EventSource(UpdateStatusEvent) + config_changed = EventSource(ConfigChangedEvent) + upgrade_charm = EventSource(UpgradeCharmEvent) + pre_series_upgrade = EventSource(PreSeriesUpgradeEvent) + post_series_upgrade = EventSource(PostSeriesUpgradeEvent) + leader_elected = EventSource(LeaderElectedEvent) + leader_settings_changed = EventSource(LeaderSettingsChangedEvent) + collect_metrics = EventSource(CollectMetricsEvent) + + +class CharmBase(Object): + """Base class that represents the Charm overall. + + Usually this initialization is done by ops.main.main() rather than Charm authors + directly instantiating a Charm. + + Args: + framework: The framework responsible for managing the Model and events for this + Charm. + key: Ignored; will remove after deprecation period of the signature change. + """ + + on = CharmEvents() + + def __init__(self, framework: Framework, key: typing.Optional = None): + super().__init__(framework, None) + + for relation_name in self.framework.meta.relations: + relation_name = relation_name.replace('-', '_') + self.on.define_event(relation_name + '_relation_created', RelationCreatedEvent) + self.on.define_event(relation_name + '_relation_joined', RelationJoinedEvent) + self.on.define_event(relation_name + '_relation_changed', RelationChangedEvent) + self.on.define_event(relation_name + '_relation_departed', RelationDepartedEvent) + self.on.define_event(relation_name + '_relation_broken', RelationBrokenEvent) + + for storage_name in self.framework.meta.storages: + storage_name = storage_name.replace('-', '_') + self.on.define_event(storage_name + '_storage_attached', StorageAttachedEvent) + self.on.define_event(storage_name + '_storage_detaching', StorageDetachingEvent) + + for action_name in self.framework.meta.actions: + action_name = action_name.replace('-', '_') + self.on.define_event(action_name + '_action', ActionEvent) + + @property + def app(self) -> model.Application: + """Application that this unit is part of.""" + return self.framework.model.app + + @property + def unit(self) -> model.Unit: + """Unit that this execution is responsible for.""" + return self.framework.model.unit + + @property + def meta(self) -> 'CharmMeta': + """CharmMeta of this charm. + """ + return self.framework.meta + + @property + def charm_dir(self) -> pathlib.Path: + """Root directory of the Charm as it is running. + """ + return self.framework.charm_dir + + +class CharmMeta: + """Object containing the metadata for the charm. + + This is read from metadata.yaml and/or actions.yaml. Generally charms will + define this information, rather than reading it at runtime. This class is + mostly for the framework to understand what the charm has defined. + + The maintainers, tags, terms, series, and extra_bindings attributes are all + lists of strings. The requires, provides, peers, relations, storage, + resources, and payloads attributes are all mappings of names to instances + of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta. + + The relations attribute is a convenience accessor which includes all of the + requires, provides, and peers RelationMeta items. If needed, the role of + the relation definition can be obtained from its role attribute. + + Attributes: + name: The name of this charm + summary: Short description of what this charm does + description: Long description for this charm + maintainers: A list of strings of the email addresses of the maintainers + of this charm. + tags: Charm store tag metadata for categories associated with this charm. + terms: Charm store terms that should be agreed to before this charm can + be deployed. (Used for things like licensing issues.) + series: The list of supported OS series that this charm can support. + The first entry in the list is the default series that will be + used by deploy if no other series is requested by the user. + subordinate: True/False whether this charm is intended to be used as a + subordinate charm. + min_juju_version: If supplied, indicates this charm needs features that + are not available in older versions of Juju. + requires: A dict of {name: :class:`RelationMeta` } for each 'requires' relation. + provides: A dict of {name: :class:`RelationMeta` } for each 'provides' relation. + peers: A dict of {name: :class:`RelationMeta` } for each 'peer' relation. + relations: A dict containing all :class:`RelationMeta` attributes (merged from other + sections) + storages: A dict of {name: :class:`StorageMeta`} for each defined storage. + resources: A dict of {name: :class:`ResourceMeta`} for each defined resource. + payloads: A dict of {name: :class:`PayloadMeta`} for each defined payload. + extra_bindings: A dict of additional named bindings that a charm can use + for network configuration. + actions: A dict of {name: :class:`ActionMeta`} for actions that the charm has defined. + Args: + raw: a mapping containing the contents of metadata.yaml + actions_raw: a mapping containing the contents of actions.yaml + """ + + def __init__(self, raw: dict = {}, actions_raw: dict = {}): + self.name = raw.get('name', '') + self.summary = raw.get('summary', '') + self.description = raw.get('description', '') + self.maintainers = [] + if 'maintainer' in raw: + self.maintainers.append(raw['maintainer']) + if 'maintainers' in raw: + self.maintainers.extend(raw['maintainers']) + self.tags = raw.get('tags', []) + self.terms = raw.get('terms', []) + self.series = raw.get('series', []) + self.subordinate = raw.get('subordinate', False) + self.min_juju_version = raw.get('min-juju-version') + self.requires = {name: RelationMeta(RelationRole.requires, name, rel) + for name, rel in raw.get('requires', {}).items()} + self.provides = {name: RelationMeta(RelationRole.provides, name, rel) + for name, rel in raw.get('provides', {}).items()} + self.peers = {name: RelationMeta(RelationRole.peer, name, rel) + for name, rel in raw.get('peers', {}).items()} + self.relations = {} + self.relations.update(self.requires) + self.relations.update(self.provides) + self.relations.update(self.peers) + self.storages = {name: StorageMeta(name, storage) + for name, storage in raw.get('storage', {}).items()} + self.resources = {name: ResourceMeta(name, res) + for name, res in raw.get('resources', {}).items()} + self.payloads = {name: PayloadMeta(name, payload) + for name, payload in raw.get('payloads', {}).items()} + self.extra_bindings = raw.get('extra-bindings', {}) + self.actions = {name: ActionMeta(name, action) for name, action in actions_raw.items()} + + @classmethod + def from_yaml( + cls, metadata: typing.Union[str, typing.TextIO], + actions: typing.Optional[typing.Union[str, typing.TextIO]] = None): + """Instantiate a CharmMeta from a YAML description of metadata.yaml. + + Args: + metadata: A YAML description of charm metadata (name, relations, etc.) + This can be a simple string, or a file-like object. (passed to `yaml.safe_load`). + actions: YAML description of Actions for this charm (eg actions.yaml) + """ + meta = _loadYaml(metadata) + raw_actions = {} + if actions is not None: + raw_actions = _loadYaml(actions) + return cls(meta, raw_actions) + + +class RelationRole(enum.Enum): + peer = 'peer' + requires = 'requires' + provides = 'provides' + + def is_peer(self) -> bool: + """Return whether the current role is peer. + + A convenience to avoid having to import charm. + """ + return self is RelationRole.peer + + +class RelationMeta: + """Object containing metadata about a relation definition. + + Should not be constructed directly by Charm code. Is gotten from one of + :attr:`CharmMeta.peers`, :attr:`CharmMeta.requires`, :attr:`CharmMeta.provides`, + or :attr:`CharmMeta.relations`. + + Attributes: + role: This is one of peer/requires/provides + relation_name: Name of this relation from metadata.yaml + interface_name: Optional definition of the interface protocol. + scope: "global" or "container" scope based on how the relation should be used. + """ + + def __init__(self, role: RelationRole, relation_name: str, raw: dict): + if not isinstance(role, RelationRole): + raise TypeError("role should be a Role, not {!r}".format(role)) + self.role = role + self.relation_name = relation_name + self.interface_name = raw['interface'] + self.scope = raw.get('scope') + + +class StorageMeta: + """Object containing metadata about a storage definition.""" + + def __init__(self, name, raw): + self.storage_name = name + self.type = raw['type'] + self.description = raw.get('description', '') + self.shared = raw.get('shared', False) + self.read_only = raw.get('read-only', False) + self.minimum_size = raw.get('minimum-size') + self.location = raw.get('location') + self.multiple_range = None + if 'multiple' in raw: + range = raw['multiple']['range'] + if '-' not in range: + self.multiple_range = (int(range), int(range)) + else: + range = range.split('-') + self.multiple_range = (int(range[0]), int(range[1]) if range[1] else None) + + +class ResourceMeta: + """Object containing metadata about a resource definition.""" + + def __init__(self, name, raw): + self.resource_name = name + self.type = raw['type'] + self.filename = raw.get('filename', None) + self.description = raw.get('description', '') + + +class PayloadMeta: + """Object containing metadata about a payload definition.""" + + def __init__(self, name, raw): + self.payload_name = name + self.type = raw['type'] + + +class ActionMeta: + """Object containing metadata about an action's definition.""" + + def __init__(self, name, raw=None): + raw = raw or {} + self.name = name + self.title = raw.get('title', '') + self.description = raw.get('description', '') + self.parameters = raw.get('params', {}) # {: } + self.required = raw.get('required', []) # [, ...] diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/framework.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/framework.py new file mode 100755 index 00000000..b7c4749f --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/framework.py @@ -0,0 +1,1067 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import collections.abc +import inspect +import keyword +import logging +import marshal +import os +import pathlib +import pdb +import re +import sys +import types +import weakref + +from ops import charm +from ops.storage import ( + NoSnapshotError, + SQLiteStorage, +) + +logger = logging.getLogger(__name__) + + +class Handle: + """Handle defines a name for an object in the form of a hierarchical path. + + The provided parent is the object (or that object's handle) that this handle + sits under, or None if the object identified by this handle stands by itself + as the root of its own hierarchy. + + The handle kind is a string that defines a namespace so objects with the + same parent and kind will have unique keys. + + The handle key is a string uniquely identifying the object. No other objects + under the same parent and kind may have the same key. + """ + + def __init__(self, parent, kind, key): + if parent and not isinstance(parent, Handle): + parent = parent.handle + self._parent = parent + self._kind = kind + self._key = key + if parent: + if key: + self._path = "{}/{}[{}]".format(parent, kind, key) + else: + self._path = "{}/{}".format(parent, kind) + else: + if key: + self._path = "{}[{}]".format(kind, key) + else: + self._path = "{}".format(kind) + + def nest(self, kind, key): + return Handle(self, kind, key) + + def __hash__(self): + return hash((self.parent, self.kind, self.key)) + + def __eq__(self, other): + return (self.parent, self.kind, self.key) == (other.parent, other.kind, other.key) + + def __str__(self): + return self.path + + @property + def parent(self): + return self._parent + + @property + def kind(self): + return self._kind + + @property + def key(self): + return self._key + + @property + def path(self): + return self._path + + @classmethod + def from_path(cls, path): + handle = None + for pair in path.split("/"): + pair = pair.split("[") + good = False + if len(pair) == 1: + kind, key = pair[0], None + good = True + elif len(pair) == 2: + kind, key = pair + if key and key[-1] == ']': + key = key[:-1] + good = True + if not good: + raise RuntimeError("attempted to restore invalid handle path {}".format(path)) + handle = Handle(handle, kind, key) + return handle + + +class EventBase: + + def __init__(self, handle): + self.handle = handle + self.deferred = False + + def defer(self): + self.deferred = True + + def snapshot(self): + """Return the snapshot data that should be persisted. + + Subclasses must override to save any custom state. + """ + return None + + def restore(self, snapshot): + """Restore the value state from the given snapshot. + + Subclasses must override to restore their custom state. + """ + self.deferred = False + + +class EventSource: + """EventSource wraps an event type with a descriptor to facilitate observing and emitting. + + It is generally used as: + + class SomethingHappened(EventBase): + pass + + class SomeObject(Object): + something_happened = EventSource(SomethingHappened) + + With that, instances of that type will offer the someobj.something_happened + attribute which is a BoundEvent and may be used to emit and observe the event. + """ + + def __init__(self, event_type): + if not isinstance(event_type, type) or not issubclass(event_type, EventBase): + raise RuntimeError( + 'Event requires a subclass of EventBase as an argument, got {}'.format(event_type)) + self.event_type = event_type + self.event_kind = None + self.emitter_type = None + + def _set_name(self, emitter_type, event_kind): + if self.event_kind is not None: + raise RuntimeError( + 'EventSource({}) reused as {}.{} and {}.{}'.format( + self.event_type.__name__, + self.emitter_type.__name__, + self.event_kind, + emitter_type.__name__, + event_kind, + )) + self.event_kind = event_kind + self.emitter_type = emitter_type + + def __get__(self, emitter, emitter_type=None): + if emitter is None: + return self + # Framework might not be available if accessed as CharmClass.on.event + # rather than charm_instance.on.event, but in that case it couldn't be + # emitted anyway, so there's no point to registering it. + framework = getattr(emitter, 'framework', None) + if framework is not None: + framework.register_type(self.event_type, emitter, self.event_kind) + return BoundEvent(emitter, self.event_type, self.event_kind) + + +class BoundEvent: + + def __repr__(self): + return ''.format( + self.event_type.__name__, + type(self.emitter).__name__, + self.event_kind, + hex(id(self)), + ) + + def __init__(self, emitter, event_type, event_kind): + self.emitter = emitter + self.event_type = event_type + self.event_kind = event_kind + + def emit(self, *args, **kwargs): + """Emit event to all registered observers. + + The current storage state is committed before and after each observer is notified. + """ + framework = self.emitter.framework + key = framework._next_event_key() + event = self.event_type(Handle(self.emitter, self.event_kind, key), *args, **kwargs) + framework._emit(event) + + +class HandleKind: + """Helper descriptor to define the Object.handle_kind field. + + The handle_kind for an object defaults to its type name, but it may + be explicitly overridden if desired. + """ + + def __get__(self, obj, obj_type): + kind = obj_type.__dict__.get("handle_kind") + if kind: + return kind + return obj_type.__name__ + + +class _Metaclass(type): + """Helper class to ensure proper instantiation of Object-derived classes. + + This class currently has a single purpose: events derived from EventSource + that are class attributes of Object-derived classes need to be told what + their name is in that class. For example, in + + class SomeObject(Object): + something_happened = EventSource(SomethingHappened) + + the instance of EventSource needs to know it's called 'something_happened'. + + Starting from python 3.6 we could use __set_name__ on EventSource for this, + but until then this (meta)class does the equivalent work. + + TODO: when we drop support for 3.5 drop this class, and rename _set_name in + EventSource to __set_name__; everything should continue to work. + + """ + + def __new__(typ, *a, **kw): + k = super().__new__(typ, *a, **kw) + # k is now the Object-derived class; loop over its class attributes + for n, v in vars(k).items(): + # we could do duck typing here if we want to support + # non-EventSource-derived shenanigans. We don't. + if isinstance(v, EventSource): + # this is what 3.6+ does automatically for us: + v._set_name(k, n) + return k + + +class Object(metaclass=_Metaclass): + + handle_kind = HandleKind() + + def __init__(self, parent, key): + kind = self.handle_kind + if isinstance(parent, Framework): + self.framework = parent + # Avoid Framework instances having a circular reference to themselves. + if self.framework is self: + self.framework = weakref.proxy(self.framework) + self.handle = Handle(None, kind, key) + else: + self.framework = parent.framework + self.handle = Handle(parent, kind, key) + self.framework._track(self) + + # TODO Detect conflicting handles here. + + @property + def model(self): + return self.framework.model + + +class ObjectEvents(Object): + """Convenience type to allow defining .on attributes at class level.""" + + handle_kind = "on" + + def __init__(self, parent=None, key=None): + if parent is not None: + super().__init__(parent, key) + else: + self._cache = weakref.WeakKeyDictionary() + + def __get__(self, emitter, emitter_type): + if emitter is None: + return self + instance = self._cache.get(emitter) + if instance is None: + # Same type, different instance, more data. Doing this unusual construct + # means people can subclass just this one class to have their own 'on'. + instance = self._cache[emitter] = type(self)(emitter) + return instance + + @classmethod + def define_event(cls, event_kind, event_type): + """Define an event on this type at runtime. + + cls: a type to define an event on. + + event_kind: an attribute name that will be used to access the + event. Must be a valid python identifier, not be a keyword + or an existing attribute. + + event_type: a type of the event to define. + + """ + prefix = 'unable to define an event with event_kind that ' + if not event_kind.isidentifier(): + raise RuntimeError(prefix + 'is not a valid python identifier: ' + event_kind) + elif keyword.iskeyword(event_kind): + raise RuntimeError(prefix + 'is a python keyword: ' + event_kind) + try: + getattr(cls, event_kind) + raise RuntimeError( + prefix + 'overlaps with an existing type {} attribute: {}'.format(cls, event_kind)) + except AttributeError: + pass + + event_descriptor = EventSource(event_type) + event_descriptor._set_name(cls, event_kind) + setattr(cls, event_kind, event_descriptor) + + def events(self): + """Return a mapping of event_kinds to bound_events for all available events. + """ + events_map = {} + # We have to iterate over the class rather than instance to allow for properties which + # might call this method (e.g., event views), leading to infinite recursion. + for attr_name, attr_value in inspect.getmembers(type(self)): + if isinstance(attr_value, EventSource): + # We actually care about the bound_event, however, since it + # provides the most info for users of this method. + event_kind = attr_name + bound_event = getattr(self, event_kind) + events_map[event_kind] = bound_event + return events_map + + def __getitem__(self, key): + return PrefixedEvents(self, key) + + +class PrefixedEvents: + + def __init__(self, emitter, key): + self._emitter = emitter + self._prefix = key.replace("-", "_") + '_' + + def __getattr__(self, name): + return getattr(self._emitter, self._prefix + name) + + +class PreCommitEvent(EventBase): + pass + + +class CommitEvent(EventBase): + pass + + +class FrameworkEvents(ObjectEvents): + pre_commit = EventSource(PreCommitEvent) + commit = EventSource(CommitEvent) + + +class NoTypeError(Exception): + + def __init__(self, handle_path): + self.handle_path = handle_path + + def __str__(self): + return "cannot restore {} since no class was registered for it".format(self.handle_path) + + +# the message to show to the user when a pdb breakpoint goes active +_BREAKPOINT_WELCOME_MESSAGE = """ +Starting pdb to debug charm operator. +Run `h` for help, `c` to continue, or `exit`/CTRL-d to abort. +Future breakpoints may interrupt execution again. +More details at https://discourse.jujucharms.com/t/debugging-charm-hooks + +""" + + +_event_regex = r'^(|.*/)on/[a-zA-Z_]+\[\d+\]$' + + +class Framework(Object): + + on = FrameworkEvents() + + # Override properties from Object so that we can set them in __init__. + model = None + meta = None + charm_dir = None + + def __init__(self, storage, charm_dir, meta, model): + + super().__init__(self, None) + + self.charm_dir = charm_dir + self.meta = meta + self.model = model + self._observers = [] # [(observer_path, method_name, parent_path, event_key)] + self._observer = weakref.WeakValueDictionary() # {observer_path: observer} + self._objects = weakref.WeakValueDictionary() + self._type_registry = {} # {(parent_path, kind): cls} + self._type_known = set() # {cls} + + if isinstance(storage, (str, pathlib.Path)): + logger.warning( + "deprecated: Framework now takes a Storage not a path") + storage = SQLiteStorage(storage) + self._storage = storage + + # We can't use the higher-level StoredState because it relies on events. + self.register_type(StoredStateData, None, StoredStateData.handle_kind) + stored_handle = Handle(None, StoredStateData.handle_kind, '_stored') + try: + self._stored = self.load_snapshot(stored_handle) + except NoSnapshotError: + self._stored = StoredStateData(self, '_stored') + self._stored['event_count'] = 0 + + # Hook into builtin breakpoint, so if Python >= 3.7, devs will be able to just do + # breakpoint(); if Python < 3.7, this doesn't affect anything + sys.breakpointhook = self.breakpoint + + # Flag to indicate that we already presented the welcome message in a debugger breakpoint + self._breakpoint_welcomed = False + + # Parse once the env var, which may be used multiple times later + debug_at = os.environ.get('JUJU_DEBUG_AT') + self._juju_debug_at = debug_at.split(',') if debug_at else () + + def close(self): + self._storage.close() + + def _track(self, obj): + """Track object and ensure it is the only object created using its handle path.""" + if obj is self: + # Framework objects don't track themselves + return + if obj.handle.path in self.framework._objects: + raise RuntimeError( + 'two objects claiming to be {} have been created'.format(obj.handle.path)) + self._objects[obj.handle.path] = obj + + def _forget(self, obj): + """Stop tracking the given object. See also _track.""" + self._objects.pop(obj.handle.path, None) + + def commit(self): + # Give a chance for objects to persist data they want to before a commit is made. + self.on.pre_commit.emit() + # Make sure snapshots are saved by instances of StoredStateData. Any possible state + # modifications in on_commit handlers of instances of other classes will not be persisted. + self.on.commit.emit() + # Save our event count after all events have been emitted. + self.save_snapshot(self._stored) + self._storage.commit() + + def register_type(self, cls, parent, kind=None): + if parent and not isinstance(parent, Handle): + parent = parent.handle + if parent: + parent_path = parent.path + else: + parent_path = None + if not kind: + kind = cls.handle_kind + self._type_registry[(parent_path, kind)] = cls + self._type_known.add(cls) + + def save_snapshot(self, value): + """Save a persistent snapshot of the provided value. + + The provided value must implement the following interface: + + value.handle = Handle(...) + value.snapshot() => {...} # Simple builtin types only. + value.restore(snapshot) # Restore custom state from prior snapshot. + """ + if type(value) not in self._type_known: + raise RuntimeError( + 'cannot save {} values before registering that type'.format(type(value).__name__)) + data = value.snapshot() + + # Use marshal as a validator, enforcing the use of simple types, as we later the + # information is really pickled, which is too error prone for future evolution of the + # stored data (e.g. if the developer stores a custom object and later changes its + # class name; when unpickling the original class will not be there and event + # data loading will fail). + try: + marshal.dumps(data) + except ValueError: + msg = "unable to save the data for {}, it must contain only simple types: {!r}" + raise ValueError(msg.format(value.__class__.__name__, data)) + + self._storage.save_snapshot(value.handle.path, data) + + def load_snapshot(self, handle): + parent_path = None + if handle.parent: + parent_path = handle.parent.path + cls = self._type_registry.get((parent_path, handle.kind)) + if not cls: + raise NoTypeError(handle.path) + data = self._storage.load_snapshot(handle.path) + obj = cls.__new__(cls) + obj.framework = self + obj.handle = handle + obj.restore(data) + self._track(obj) + return obj + + def drop_snapshot(self, handle): + self._storage.drop_snapshot(handle.path) + + def observe(self, bound_event: BoundEvent, observer: types.MethodType): + """Register observer to be called when bound_event is emitted. + + The bound_event is generally provided as an attribute of the object that emits + the event, and is created in this style: + + class SomeObject: + something_happened = Event(SomethingHappened) + + That event may be observed as: + + framework.observe(someobj.something_happened, self._on_something_happened) + + Raises: + RuntimeError: if bound_event or observer are the wrong type. + """ + if not isinstance(bound_event, BoundEvent): + raise RuntimeError( + 'Framework.observe requires a BoundEvent as second parameter, got {}'.format( + bound_event)) + if not isinstance(observer, types.MethodType): + # help users of older versions of the framework + if isinstance(observer, charm.CharmBase): + raise TypeError( + 'observer methods must now be explicitly provided;' + ' please replace observe(self.on.{0}, self)' + ' with e.g. observe(self.on.{0}, self._on_{0})'.format( + bound_event.event_kind)) + raise RuntimeError( + 'Framework.observe requires a method as third parameter, got {}'.format(observer)) + + event_type = bound_event.event_type + event_kind = bound_event.event_kind + emitter = bound_event.emitter + + self.register_type(event_type, emitter, event_kind) + + if hasattr(emitter, "handle"): + emitter_path = emitter.handle.path + else: + raise RuntimeError( + 'event emitter {} must have a "handle" attribute'.format(type(emitter).__name__)) + + # Validate that the method has an acceptable call signature. + sig = inspect.signature(observer) + # Self isn't included in the params list, so the first arg will be the event. + extra_params = list(sig.parameters.values())[1:] + + method_name = observer.__name__ + observer = observer.__self__ + if not sig.parameters: + raise TypeError( + '{}.{} must accept event parameter'.format(type(observer).__name__, method_name)) + elif any(param.default is inspect.Parameter.empty for param in extra_params): + # Allow for additional optional params, since there's no reason to exclude them, but + # required params will break. + raise TypeError( + '{}.{} has extra required parameter'.format(type(observer).__name__, method_name)) + + # TODO Prevent the exact same parameters from being registered more than once. + + self._observer[observer.handle.path] = observer + self._observers.append((observer.handle.path, method_name, emitter_path, event_kind)) + + def _next_event_key(self): + """Return the next event key that should be used, incrementing the internal counter.""" + # Increment the count first; this means the keys will start at 1, and 0 + # means no events have been emitted. + self._stored['event_count'] += 1 + return str(self._stored['event_count']) + + def _emit(self, event): + """See BoundEvent.emit for the public way to call this.""" + + saved = False + event_path = event.handle.path + event_kind = event.handle.kind + parent_path = event.handle.parent.path + # TODO Track observers by (parent_path, event_kind) rather than as a list of + # all observers. Avoiding linear search through all observers for every event + for observer_path, method_name, _parent_path, _event_kind in self._observers: + if _parent_path != parent_path: + continue + if _event_kind and _event_kind != event_kind: + continue + if not saved: + # Save the event for all known observers before the first notification + # takes place, so that either everyone interested sees it, or nobody does. + self.save_snapshot(event) + saved = True + # Again, only commit this after all notices are saved. + self._storage.save_notice(event_path, observer_path, method_name) + if saved: + self._reemit(event_path) + + def reemit(self): + """Reemit previously deferred events to the observers that deferred them. + + Only the specific observers that have previously deferred the event will be + notified again. Observers that asked to be notified about events after it's + been first emitted won't be notified, as that would mean potentially observing + events out of order. + """ + self._reemit() + + def _reemit(self, single_event_path=None): + last_event_path = None + deferred = True + for event_path, observer_path, method_name in self._storage.notices(single_event_path): + event_handle = Handle.from_path(event_path) + + if last_event_path != event_path: + if not deferred and last_event_path is not None: + self._storage.drop_snapshot(last_event_path) + last_event_path = event_path + deferred = False + + try: + event = self.load_snapshot(event_handle) + except NoTypeError: + self._storage.drop_notice(event_path, observer_path, method_name) + continue + + event.deferred = False + observer = self._observer.get(observer_path) + if observer: + custom_handler = getattr(observer, method_name, None) + if custom_handler: + event_is_from_juju = isinstance(event, charm.HookEvent) + event_is_action = isinstance(event, charm.ActionEvent) + if (event_is_from_juju or event_is_action) and 'hook' in self._juju_debug_at: + # Present the welcome message and run under PDB. + self._show_debug_code_message() + pdb.runcall(custom_handler, event) + else: + # Regular call to the registered method. + custom_handler(event) + + if event.deferred: + deferred = True + else: + self._storage.drop_notice(event_path, observer_path, method_name) + # We intentionally consider this event to be dead and reload it from + # scratch in the next path. + self.framework._forget(event) + + if not deferred and last_event_path is not None: + self._storage.drop_snapshot(last_event_path) + + def _show_debug_code_message(self): + """Present the welcome message (only once!) when using debugger functionality.""" + if not self._breakpoint_welcomed: + self._breakpoint_welcomed = True + print(_BREAKPOINT_WELCOME_MESSAGE, file=sys.stderr, end='') + + def breakpoint(self, name=None): + """Add breakpoint, optionally named, at the place where this method is called. + + For the breakpoint to be activated the JUJU_DEBUG_AT environment variable + must be set to "all" or to the specific name parameter provided, if any. In every + other situation calling this method does nothing. + + The framework also provides a standard breakpoint named "hook", that will + stop execution when a hook event is about to be handled. + + For those reasons, the "all" and "hook" breakpoint names are reserved. + """ + # If given, validate the name comply with all the rules + if name is not None: + if not isinstance(name, str): + raise TypeError('breakpoint names must be strings') + if name in ('hook', 'all'): + raise ValueError('breakpoint names "all" and "hook" are reserved') + if not re.match(r'^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$', name): + raise ValueError('breakpoint names must look like "foo" or "foo-bar"') + + indicated_breakpoints = self._juju_debug_at + if not indicated_breakpoints: + return + + if 'all' in indicated_breakpoints or name in indicated_breakpoints: + self._show_debug_code_message() + + # If we call set_trace() directly it will open the debugger *here*, so indicating + # it to use our caller's frame + code_frame = inspect.currentframe().f_back + pdb.Pdb().set_trace(code_frame) + else: + logger.warning( + "Breakpoint %r skipped (not found in the requested breakpoints: %s)", + name, indicated_breakpoints) + + def remove_unreferenced_events(self): + """Remove events from storage that are not referenced. + + In older versions of the framework, events that had no observers would get recorded but + never deleted. This makes a best effort to find these events and remove them from the + database. + """ + event_regex = re.compile(_event_regex) + to_remove = [] + for handle_path in self._storage.list_snapshots(): + if event_regex.match(handle_path): + notices = self._storage.notices(handle_path) + if next(notices, None) is None: + # There are no notices for this handle_path, it is valid to remove it + to_remove.append(handle_path) + for handle_path in to_remove: + self._storage.drop_snapshot(handle_path) + + +class StoredStateData(Object): + + def __init__(self, parent, attr_name): + super().__init__(parent, attr_name) + self._cache = {} + self.dirty = False + + def __getitem__(self, key): + return self._cache.get(key) + + def __setitem__(self, key, value): + self._cache[key] = value + self.dirty = True + + def __contains__(self, key): + return key in self._cache + + def snapshot(self): + return self._cache + + def restore(self, snapshot): + self._cache = snapshot + self.dirty = False + + def on_commit(self, event): + if self.dirty: + self.framework.save_snapshot(self) + self.dirty = False + + +class BoundStoredState: + + def __init__(self, parent, attr_name): + parent.framework.register_type(StoredStateData, parent) + + handle = Handle(parent, StoredStateData.handle_kind, attr_name) + try: + data = parent.framework.load_snapshot(handle) + except NoSnapshotError: + data = StoredStateData(parent, attr_name) + + # __dict__ is used to avoid infinite recursion. + self.__dict__["_data"] = data + self.__dict__["_attr_name"] = attr_name + + parent.framework.observe(parent.framework.on.commit, self._data.on_commit) + + def __getattr__(self, key): + # "on" is the only reserved key that can't be used in the data map. + if key == "on": + return self._data.on + if key not in self._data: + raise AttributeError("attribute '{}' is not stored".format(key)) + return _wrap_stored(self._data, self._data[key]) + + def __setattr__(self, key, value): + if key == "on": + raise AttributeError("attribute 'on' is reserved and cannot be set") + + value = _unwrap_stored(self._data, value) + + if not isinstance(value, (type(None), int, float, str, bytes, list, dict, set)): + raise AttributeError( + 'attribute {!r} cannot be a {}: must be int/float/dict/list/etc'.format( + key, type(value).__name__)) + + self._data[key] = _unwrap_stored(self._data, value) + + def set_default(self, **kwargs): + """"Set the value of any given key if it has not already been set""" + for k, v in kwargs.items(): + if k not in self._data: + self._data[k] = v + + +class StoredState: + """A class used to store data the charm needs persisted across invocations. + + Example:: + + class MyClass(Object): + _stored = StoredState() + + Instances of `MyClass` can transparently save state between invocations by + setting attributes on `_stored`. Initial state should be set with + `set_default` on the bound object, that is:: + + class MyClass(Object): + _stored = StoredState() + + def __init__(self, parent, key): + super().__init__(parent, key) + self._stored.set_default(seen=set()) + self.framework.observe(self.on.seen, self._on_seen) + + def _on_seen(self, event): + self._stored.seen.add(event.uuid) + + """ + + def __init__(self): + self.parent_type = None + self.attr_name = None + + def __get__(self, parent, parent_type=None): + if self.parent_type is not None and self.parent_type not in parent_type.mro(): + # the StoredState instance is being shared between two unrelated classes + # -> unclear what is exepcted of us -> bail out + raise RuntimeError( + 'StoredState shared by {} and {}'.format( + self.parent_type.__name__, parent_type.__name__)) + + if parent is None: + # accessing via the class directly (e.g. MyClass.stored) + return self + + bound = None + if self.attr_name is not None: + bound = parent.__dict__.get(self.attr_name) + if bound is not None: + # we already have the thing from a previous pass, huzzah + return bound + + # need to find ourselves amongst the parent's bases + for cls in parent_type.mro(): + for attr_name, attr_value in cls.__dict__.items(): + if attr_value is not self: + continue + # we've found ourselves! is it the first time? + if bound is not None: + # the StoredState instance is being stored in two different + # attributes -> unclear what is expected of us -> bail out + raise RuntimeError("StoredState shared by {0}.{1} and {0}.{2}".format( + cls.__name__, self.attr_name, attr_name)) + # we've found ourselves for the first time; save where, and bind the object + self.attr_name = attr_name + self.parent_type = cls + bound = BoundStoredState(parent, attr_name) + + if bound is not None: + # cache the bound object to avoid the expensive lookup the next time + # (don't use setattr, to keep things symmetric with the fast-path lookup above) + parent.__dict__[self.attr_name] = bound + return bound + + raise AttributeError( + 'cannot find {} attribute in type {}'.format( + self.__class__.__name__, parent_type.__name__)) + + +def _wrap_stored(parent_data, value): + t = type(value) + if t is dict: + return StoredDict(parent_data, value) + if t is list: + return StoredList(parent_data, value) + if t is set: + return StoredSet(parent_data, value) + return value + + +def _unwrap_stored(parent_data, value): + t = type(value) + if t is StoredDict or t is StoredList or t is StoredSet: + return value._under + return value + + +class StoredDict(collections.abc.MutableMapping): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def __getitem__(self, key): + return _wrap_stored(self._stored_data, self._under[key]) + + def __setitem__(self, key, value): + self._under[key] = _unwrap_stored(self._stored_data, value) + self._stored_data.dirty = True + + def __delitem__(self, key): + del self._under[key] + self._stored_data.dirty = True + + def __iter__(self): + return self._under.__iter__() + + def __len__(self): + return len(self._under) + + def __eq__(self, other): + if isinstance(other, StoredDict): + return self._under == other._under + elif isinstance(other, collections.abc.Mapping): + return self._under == other + else: + return NotImplemented + + +class StoredList(collections.abc.MutableSequence): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def __getitem__(self, index): + return _wrap_stored(self._stored_data, self._under[index]) + + def __setitem__(self, index, value): + self._under[index] = _unwrap_stored(self._stored_data, value) + self._stored_data.dirty = True + + def __delitem__(self, index): + del self._under[index] + self._stored_data.dirty = True + + def __len__(self): + return len(self._under) + + def insert(self, index, value): + self._under.insert(index, value) + self._stored_data.dirty = True + + def append(self, value): + self._under.append(value) + self._stored_data.dirty = True + + def __eq__(self, other): + if isinstance(other, StoredList): + return self._under == other._under + elif isinstance(other, collections.abc.Sequence): + return self._under == other + else: + return NotImplemented + + def __lt__(self, other): + if isinstance(other, StoredList): + return self._under < other._under + elif isinstance(other, collections.abc.Sequence): + return self._under < other + else: + return NotImplemented + + def __le__(self, other): + if isinstance(other, StoredList): + return self._under <= other._under + elif isinstance(other, collections.abc.Sequence): + return self._under <= other + else: + return NotImplemented + + def __gt__(self, other): + if isinstance(other, StoredList): + return self._under > other._under + elif isinstance(other, collections.abc.Sequence): + return self._under > other + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, StoredList): + return self._under >= other._under + elif isinstance(other, collections.abc.Sequence): + return self._under >= other + else: + return NotImplemented + + +class StoredSet(collections.abc.MutableSet): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def add(self, key): + self._under.add(key) + self._stored_data.dirty = True + + def discard(self, key): + self._under.discard(key) + self._stored_data.dirty = True + + def __contains__(self, key): + return key in self._under + + def __iter__(self): + return self._under.__iter__() + + def __len__(self): + return len(self._under) + + @classmethod + def _from_iterable(cls, it): + """Construct an instance of the class from any iterable input. + + Per https://docs.python.org/3/library/collections.abc.html + if the Set mixin is being used in a class with a different constructor signature, + you will need to override _from_iterable() with a classmethod that can construct + new instances from an iterable argument. + """ + return set(it) + + def __le__(self, other): + if isinstance(other, StoredSet): + return self._under <= other._under + elif isinstance(other, collections.abc.Set): + return self._under <= other + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, StoredSet): + return self._under >= other._under + elif isinstance(other, collections.abc.Set): + return self._under >= other + else: + return NotImplemented + + def __eq__(self, other): + if isinstance(other, StoredSet): + return self._under == other._under + elif isinstance(other, collections.abc.Set): + return self._under == other + else: + return NotImplemented diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/jujuversion.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/jujuversion.py new file mode 100755 index 00000000..b2b8177d --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/jujuversion.py @@ -0,0 +1,98 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +from functools import total_ordering + + +@total_ordering +class JujuVersion: + + PATTERN = r'''^ + (?P\d{1,9})\.(?P\d{1,9}) # and numbers are always there + ((?:\.|-(?P[a-z]+))(?P\d{1,9}))? # sometimes with . or - + (\.(?P\d{1,9}))?$ # and sometimes with a number. + ''' + + def __init__(self, version): + m = re.match(self.PATTERN, version, re.VERBOSE) + if not m: + raise RuntimeError('"{}" is not a valid Juju version string'.format(version)) + + d = m.groupdict() + self.major = int(m.group('major')) + self.minor = int(m.group('minor')) + self.tag = d['tag'] or '' + self.patch = int(d['patch'] or 0) + self.build = int(d['build'] or 0) + + def __repr__(self): + if self.tag: + s = '{}.{}-{}{}'.format(self.major, self.minor, self.tag, self.patch) + else: + s = '{}.{}.{}'.format(self.major, self.minor, self.patch) + if self.build > 0: + s += '.{}'.format(self.build) + return s + + def __eq__(self, other): + if self is other: + return True + if isinstance(other, str): + other = type(self)(other) + elif not isinstance(other, JujuVersion): + raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other)) + return ( + self.major == other.major + and self.minor == other.minor + and self.tag == other.tag + and self.build == other.build + and self.patch == other.patch) + + def __lt__(self, other): + if self is other: + return False + if isinstance(other, str): + other = type(self)(other) + elif not isinstance(other, JujuVersion): + raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other)) + + if self.major != other.major: + return self.major < other.major + elif self.minor != other.minor: + return self.minor < other.minor + elif self.tag != other.tag: + if not self.tag: + return False + elif not other.tag: + return True + return self.tag < other.tag + elif self.patch != other.patch: + return self.patch < other.patch + elif self.build != other.build: + return self.build < other.build + return False + + @classmethod + def from_environ(cls) -> 'JujuVersion': + """Build a JujuVersion from JUJU_VERSION.""" + v = os.environ.get('JUJU_VERSION') + if not v: + raise RuntimeError('environ has no JUJU_VERSION') + return cls(v) + + def has_app_data(self) -> bool: + """Determine whether this juju version knows about app data.""" + return (self.major, self.minor, self.patch) >= (2, 7, 0) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/lib/__init__.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/lib/__init__.py new file mode 100644 index 00000000..edb9fcac --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/lib/__init__.py @@ -0,0 +1,194 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import os +import re + +from ast import literal_eval +from importlib.util import module_from_spec +from importlib.machinery import ModuleSpec +from pkgutil import get_importer +from types import ModuleType + + +_libraries = None + +_libline_re = re.compile(r'''^LIB([A-Z]+)\s*=\s*([0-9]+|['"][a-zA-Z0-9_.\-@]+['"])''') +_libname_re = re.compile(r'''^[a-z][a-z0-9]+$''') + +# Not perfect, but should do for now. +_libauthor_re = re.compile(r'''^[A-Za-z0-9_+.-]+@[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z]{2,3}$''') + + +def use(name: str, api: int, author: str) -> ModuleType: + """Use a library from the ops libraries. + + Args: + name: the name of the library requested. + api: the API version of the library. + author: the author of the library. If not given, requests the + one in the standard library. + Raises: + ImportError: if the library cannot be found. + TypeError: if the name, api, or author are the wrong type. + ValueError: if the name, api, or author are invalid. + """ + if not isinstance(name, str): + raise TypeError("invalid library name: {!r} (must be a str)".format(name)) + if not isinstance(author, str): + raise TypeError("invalid library author: {!r} (must be a str)".format(author)) + if not isinstance(api, int): + raise TypeError("invalid library API: {!r} (must be an int)".format(api)) + if api < 0: + raise ValueError('invalid library api: {} (must be ≥0)'.format(api)) + if not _libname_re.match(name): + raise ValueError("invalid library name: {!r} (chars and digits only)".format(name)) + if not _libauthor_re.match(author): + raise ValueError("invalid library author email: {!r}".format(author)) + + if _libraries is None: + autoimport() + + versions = _libraries.get((name, author), ()) + for lib in versions: + if lib.api == api: + return lib.import_module() + + others = ', '.join(str(lib.api) for lib in versions) + if others: + msg = 'cannot find "{}" from "{}" with API version {} (have {})'.format( + name, author, api, others) + else: + msg = 'cannot find library "{}" from "{}"'.format(name, author) + + raise ImportError(msg, name=name) + + +def autoimport(): + """Find all libs in the path and enable use of them. + + You only need to call this if you've installed a package or + otherwise changed sys.path in the current run, and need to see the + changes. Otherwise libraries are found on first call of `use`. + """ + global _libraries + _libraries = {} + for spec in _find_all_specs(sys.path): + lib = _parse_lib(spec) + if lib is None: + continue + + versions = _libraries.setdefault((lib.name, lib.author), []) + versions.append(lib) + versions.sort(reverse=True) + + +def _find_all_specs(path): + for sys_dir in path: + if sys_dir == "": + sys_dir = "." + try: + top_dirs = os.listdir(sys_dir) + except OSError: + continue + for top_dir in top_dirs: + opslib = os.path.join(sys_dir, top_dir, 'opslib') + try: + lib_dirs = os.listdir(opslib) + except OSError: + continue + finder = get_importer(opslib) + if finder is None or not hasattr(finder, 'find_spec'): + continue + for lib_dir in lib_dirs: + spec = finder.find_spec(lib_dir) + if spec is None: + continue + if spec.loader is None: + # a namespace package; not supported + continue + yield spec + + +# only the first this many lines of a file are looked at for the LIB* constants +_MAX_LIB_LINES = 99 + + +def _parse_lib(spec): + if spec.origin is None: + return None + + _expected = {'NAME': str, 'AUTHOR': str, 'API': int, 'PATCH': int} + + try: + with open(spec.origin, 'rt', encoding='utf-8') as f: + libinfo = {} + for n, line in enumerate(f): + if len(libinfo) == len(_expected): + break + if n > _MAX_LIB_LINES: + return None + m = _libline_re.match(line) + if m is None: + continue + key, value = m.groups() + if key in _expected: + value = literal_eval(value) + if not isinstance(value, _expected[key]): + return None + libinfo[key] = value + else: + if len(libinfo) != len(_expected): + return None + except Exception: + return None + + return _Lib(spec, libinfo['NAME'], libinfo['AUTHOR'], libinfo['API'], libinfo['PATCH']) + + +class _Lib: + + def __init__(self, spec: ModuleSpec, name: str, author: str, api: int, patch: int): + self.spec = spec + self.name = name + self.author = author + self.api = api + self.patch = patch + + self._module = None + + def __repr__(self): + return "<_Lib {0.name} by {0.author}, API {0.api}, patch {0.patch}>".format(self) + + def import_module(self) -> ModuleType: + if self._module is None: + module = module_from_spec(self.spec) + self.spec.loader.exec_module(module) + self._module = module + return self._module + + def __eq__(self, other): + if not isinstance(other, _Lib): + return NotImplemented + a = (self.name, self.author, self.api, self.patch) + b = (other.name, other.author, other.api, other.patch) + return a == b + + def __lt__(self, other): + if not isinstance(other, _Lib): + return NotImplemented + a = (self.name, self.author, self.api, self.patch) + b = (other.name, other.author, other.api, other.patch) + return a < b diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/log.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/log.py new file mode 100644 index 00000000..4aac5543 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/log.py @@ -0,0 +1,51 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import logging + + +class JujuLogHandler(logging.Handler): + """A handler for sending logs to Juju via juju-log.""" + + def __init__(self, model_backend, level=logging.DEBUG): + super().__init__(level) + self.model_backend = model_backend + + def emit(self, record): + self.model_backend.juju_log(record.levelname, self.format(record)) + + +def setup_root_logging(model_backend, debug=False): + """Setup python logging to forward messages to juju-log. + + By default, logging is set to DEBUG level, and messages will be filtered by Juju. + Charmers can also set their own default log level with:: + + logging.getLogger().setLevel(logging.INFO) + + model_backend -- a ModelBackend to use for juju-log + debug -- if True, write logs to stderr as well as to juju-log. + """ + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(JujuLogHandler(model_backend)) + if debug: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + sys.excepthook = lambda etype, value, tb: logger.error( + "Uncaught exception while in charm code:", exc_info=(etype, value, tb)) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/main.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/main.py new file mode 100755 index 00000000..6dc31c35 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/main.py @@ -0,0 +1,348 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import logging +import os +import subprocess +import sys +import warnings +from pathlib import Path + +import yaml + +import ops.charm +import ops.framework +import ops.model +import ops.storage + +from ops.log import setup_root_logging + +CHARM_STATE_FILE = '.unit-state.db' + + +logger = logging.getLogger() + + +def _get_charm_dir(): + charm_dir = os.environ.get("JUJU_CHARM_DIR") + if charm_dir is None: + # Assume $JUJU_CHARM_DIR/lib/op/main.py structure. + charm_dir = Path('{}/../../..'.format(__file__)).resolve() + else: + charm_dir = Path(charm_dir).resolve() + return charm_dir + + +def _create_event_link(charm, bound_event): + """Create a symlink for a particular event. + + charm -- A charm object. + bound_event -- An event for which to create a symlink. + """ + if issubclass(bound_event.event_type, ops.charm.HookEvent): + event_dir = charm.framework.charm_dir / 'hooks' + event_path = event_dir / bound_event.event_kind.replace('_', '-') + elif issubclass(bound_event.event_type, ops.charm.ActionEvent): + if not bound_event.event_kind.endswith("_action"): + raise RuntimeError( + 'action event name {} needs _action suffix'.format(bound_event.event_kind)) + event_dir = charm.framework.charm_dir / 'actions' + # The event_kind is suffixed with "_action" while the executable is not. + event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-') + else: + raise RuntimeError( + 'cannot create a symlink: unsupported event type {}'.format(bound_event.event_type)) + + event_dir.mkdir(exist_ok=True) + if not event_path.exists(): + # CPython has different implementations for populating sys.argv[0] for Linux and Windows. + # For Windows it is always an absolute path (any symlinks are resolved) + # while for Linux it can be a relative path. + target_path = os.path.relpath(os.path.realpath(sys.argv[0]), str(event_dir)) + + # Ignore the non-symlink files or directories + # assuming the charm author knows what they are doing. + logger.debug( + 'Creating a new relative symlink at %s pointing to %s', + event_path, target_path) + event_path.symlink_to(target_path) + + +def _setup_event_links(charm_dir, charm): + """Set up links for supported events that originate from Juju. + + Whether a charm can handle an event or not can be determined by + introspecting which events are defined on it. + + Hooks or actions are created as symlinks to the charm code file + which is determined by inspecting symlinks provided by the charm + author at hooks/install or hooks/start. + + charm_dir -- A root directory of the charm. + charm -- An instance of the Charm class. + + """ + for bound_event in charm.on.events().values(): + # Only events that originate from Juju need symlinks. + if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)): + _create_event_link(charm, bound_event) + + +def _emit_charm_event(charm, event_name): + """Emits a charm event based on a Juju event name. + + charm -- A charm instance to emit an event from. + event_name -- A Juju event name to emit on a charm. + """ + event_to_emit = None + try: + event_to_emit = getattr(charm.on, event_name) + except AttributeError: + logger.debug("Event %s not defined for %s.", event_name, charm) + + # If the event is not supported by the charm implementation, do + # not error out or try to emit it. This is to support rollbacks. + if event_to_emit is not None: + args, kwargs = _get_event_args(charm, event_to_emit) + logger.debug('Emitting Juju event %s.', event_name) + event_to_emit.emit(*args, **kwargs) + + +def _get_event_args(charm, bound_event): + event_type = bound_event.event_type + model = charm.framework.model + + if issubclass(event_type, ops.charm.RelationEvent): + relation_name = os.environ['JUJU_RELATION'] + relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1]) + relation = model.get_relation(relation_name, relation_id) + else: + relation = None + + remote_app_name = os.environ.get('JUJU_REMOTE_APP', '') + remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '') + if remote_app_name or remote_unit_name: + if not remote_app_name: + if '/' not in remote_unit_name: + raise RuntimeError('invalid remote unit name: {}'.format(remote_unit_name)) + remote_app_name = remote_unit_name.split('/')[0] + args = [relation, model.get_app(remote_app_name)] + if remote_unit_name: + args.append(model.get_unit(remote_unit_name)) + return args, {} + elif relation: + return [relation], {} + return [], {} + + +class _Dispatcher: + """Encapsulate how to figure out what event Juju wants us to run. + + Also knows how to run “legacy” hooks when Juju called us via a top-level + ``dispatch`` binary. + + Args: + charm_dir: the toplevel directory of the charm + + Attributes: + event_name: the name of the event to run + is_dispatch_aware: are we running under a Juju that knows about the + dispatch binary? + + """ + + def __init__(self, charm_dir: Path): + self._charm_dir = charm_dir + self._exec_path = Path(sys.argv[0]) + + if 'JUJU_DISPATCH_PATH' in os.environ and (charm_dir / 'dispatch').exists(): + self._init_dispatch() + else: + self._init_legacy() + + def ensure_event_links(self, charm): + """Make sure necessary symlinks are present on disk""" + + if self.is_dispatch_aware: + # links aren't needed + return + + # When a charm is force-upgraded and a unit is in an error state Juju + # does not run upgrade-charm and instead runs the failed hook followed + # by config-changed. Given the nature of force-upgrading the hook setup + # code is not triggered on config-changed. + # + # 'start' event is included as Juju does not fire the install event for + # K8s charms (see LP: #1854635). + if (self.event_name in ('install', 'start', 'upgrade_charm') + or self.event_name.endswith('_storage_attached')): + _setup_event_links(self._charm_dir, charm) + + def run_any_legacy_hook(self): + """Run any extant legacy hook. + + If there is both a dispatch file and a legacy hook for the + current event, run the wanted legacy hook. + """ + + if not self.is_dispatch_aware: + # we *are* the legacy hook + return + + dispatch_path = self._charm_dir / self._dispatch_path + if not dispatch_path.exists(): + logger.debug("Legacy %s does not exist.", self._dispatch_path) + return + + # super strange that there isn't an is_executable + if not os.access(str(dispatch_path), os.X_OK): + logger.warning("Legacy %s exists but is not executable.", self._dispatch_path) + return + + if dispatch_path.resolve() == self._exec_path.resolve(): + logger.debug("Legacy %s is just a link to ourselves.", self._dispatch_path) + return + + argv = sys.argv.copy() + argv[0] = str(dispatch_path) + logger.info("Running legacy %s.", self._dispatch_path) + try: + subprocess.run(argv, check=True) + except subprocess.CalledProcessError as e: + logger.warning( + "Legacy %s exited with status %d.", + self._dispatch_path, e.returncode) + sys.exit(e.returncode) + else: + logger.debug("Legacy %s exited with status 0.", self._dispatch_path) + + def _set_name_from_path(self, path: Path): + """Sets the name attribute to that which can be inferred from the given path.""" + name = path.name.replace('-', '_') + if path.parent.name == 'actions': + name = '{}_action'.format(name) + self.event_name = name + + def _init_legacy(self): + """Set up the 'legacy' dispatcher. + + The current Juju doesn't know about 'dispatch' and calls hooks + explicitly. + """ + self.is_dispatch_aware = False + self._set_name_from_path(self._exec_path) + + def _init_dispatch(self): + """Set up the new 'dispatch' dispatcher. + + The current Juju will run 'dispatch' if it exists, and otherwise fall + back to the old behaviour. + + JUJU_DISPATCH_PATH will be set to the wanted hook, e.g. hooks/install, + in both cases. + """ + self._dispatch_path = Path(os.environ['JUJU_DISPATCH_PATH']) + + if 'OPERATOR_DISPATCH' in os.environ: + logger.debug("Charm called itself via %s.", self._dispatch_path) + sys.exit(0) + os.environ['OPERATOR_DISPATCH'] = '1' + + self.is_dispatch_aware = True + self._set_name_from_path(self._dispatch_path) + + def is_restricted_context(self): + """"Return True if we are running in a restricted Juju context. + + When in a restricted context, most commands (relation-get, config-get, + state-get) are not available. As such, we change how we interact with + Juju. + """ + return self.event_name in ('collect_metrics',) + + +def main(charm_class, use_juju_for_storage=False): + """Setup the charm and dispatch the observed event. + + The event name is based on the way this executable was called (argv[0]). + """ + charm_dir = _get_charm_dir() + + model_backend = ops.model._ModelBackend() + debug = ('JUJU_DEBUG' in os.environ) + setup_root_logging(model_backend, debug=debug) + logger.debug("Operator Framework %s up and running.", ops.__version__) + + dispatcher = _Dispatcher(charm_dir) + dispatcher.run_any_legacy_hook() + + metadata = (charm_dir / 'metadata.yaml').read_text() + actions_meta = charm_dir / 'actions.yaml' + if actions_meta.exists(): + actions_metadata = actions_meta.read_text() + else: + actions_metadata = None + + if not yaml.__with_libyaml__: + logger.debug('yaml does not have libyaml extensions, using slower pure Python yaml loader') + meta = ops.charm.CharmMeta.from_yaml(metadata, actions_metadata) + model = ops.model.Model(meta, model_backend) + + # TODO: If Juju unit agent crashes after exit(0) from the charm code + # the framework will commit the snapshot but Juju will not commit its + # operation. + charm_state_path = charm_dir / CHARM_STATE_FILE + if use_juju_for_storage: + if dispatcher.is_restricted_context(): + # TODO: jam 2020-06-30 This unconditionally avoids running a collect metrics event + # Though we eventually expect that juju will run collect-metrics in a + # non-restricted context. Once we can determine that we are running collect-metrics + # in a non-restricted context, we should fire the event as normal. + logger.debug('"%s" is not supported when using Juju for storage\n' + 'see: https://github.com/canonical/operator/issues/348', + dispatcher.event_name) + # Note that we don't exit nonzero, because that would cause Juju to rerun the hook + return + store = ops.storage.JujuStorage() + else: + store = ops.storage.SQLiteStorage(charm_state_path) + framework = ops.framework.Framework(store, charm_dir, meta, model) + try: + sig = inspect.signature(charm_class) + try: + sig.bind(framework) + except TypeError: + msg = ( + "the second argument, 'key', has been deprecated and will be " + "removed after the 0.7 release") + warnings.warn(msg, DeprecationWarning) + charm = charm_class(framework, None) + else: + charm = charm_class(framework) + dispatcher.ensure_event_links(charm) + + # TODO: Remove the collect_metrics check below as soon as the relevant + # Juju changes are made. + # + # Skip reemission of deferred events for collect-metrics events because + # they do not have the full access to all hook tools. + if not dispatcher.is_restricted_context(): + framework.reemit() + + _emit_charm_event(charm, dispatcher.event_name) + + framework.commit() + finally: + framework.close() diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/model.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/model.py new file mode 100644 index 00000000..b96e8915 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/model.py @@ -0,0 +1,1237 @@ +# Copyright 2019 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import decimal +import ipaddress +import json +import os +import re +import shutil +import tempfile +import time +import typing +import weakref + +from abc import ABC, abstractmethod +from collections.abc import Mapping, MutableMapping +from pathlib import Path +from subprocess import run, PIPE, CalledProcessError + +import ops +from ops.jujuversion import JujuVersion + + +class Model: + """Represents the Juju Model as seen from this unit. + + This should not be instantiated directly by Charmers, but can be accessed as `self.model` + from any class that derives from Object. + + Attributes: + unit: A :class:`Unit` that represents the unit that is running this code (eg yourself) + app: A :class:`Application` that represents the application this unit is a part of. + relations: Mapping of endpoint to list of :class:`Relation` answering the question + "what am I currently related to". See also :meth:`.get_relation` + config: A dict of the config for the current application. + resources: Access to resources for this charm. Use ``model.resources.fetch(resource_name)`` + to get the path on disk where the resource can be found. + storages: Mapping of storage_name to :class:`Storage` for the storage points defined in + metadata.yaml + pod: Used to get access to ``model.pod.set_spec`` to set the container specification + for Kubernetes charms. + """ + + def __init__(self, meta: 'ops.charm.CharmMeta', backend: '_ModelBackend'): + self._cache = _ModelCache(backend) + self._backend = backend + self.unit = self.get_unit(self._backend.unit_name) + self.app = self.unit.app + self.relations = RelationMapping(meta.relations, self.unit, self._backend, self._cache) + self.config = ConfigData(self._backend) + self.resources = Resources(list(meta.resources), self._backend) + self.pod = Pod(self._backend) + self.storages = StorageMapping(list(meta.storages), self._backend) + self._bindings = BindingMapping(self._backend) + + @property + def name(self) -> str: + """Return the name of the Model that this unit is running in. + + This is read from the environment variable ``JUJU_MODEL_NAME``. + """ + return self._backend.model_name + + def get_unit(self, unit_name: str) -> 'Unit': + """Get an arbitrary unit by name. + + Internally this uses a cache, so asking for the same unit two times will + return the same object. + """ + return self._cache.get(Unit, unit_name) + + def get_app(self, app_name: str) -> 'Application': + """Get an application by name. + + Internally this uses a cache, so asking for the same application two times will + return the same object. + """ + return self._cache.get(Application, app_name) + + def get_relation( + self, relation_name: str, + relation_id: typing.Optional[int] = None) -> 'Relation': + """Get a specific Relation instance. + + If relation_id is not given, this will return the Relation instance if the + relation is established only once or None if it is not established. If this + same relation is established multiple times the error TooManyRelatedAppsError is raised. + + Args: + relation_name: The name of the endpoint for this charm + relation_id: An identifier for a specific relation. Used to disambiguate when a + given application has more than one relation on a given endpoint. + Raises: + TooManyRelatedAppsError: is raised if there is more than one relation to the + supplied relation_name and no relation_id was supplied + """ + return self.relations._get_unique(relation_name, relation_id) + + def get_binding(self, binding_key: typing.Union[str, 'Relation']) -> 'Binding': + """Get a network space binding. + + Args: + binding_key: The relation name or instance to obtain bindings for. + Returns: + If ``binding_key`` is a relation name, the method returns the default binding + for that relation. If a relation instance is provided, the method first looks + up a more specific binding for that specific relation ID, and if none is found + falls back to the default binding for the relation name. + """ + return self._bindings.get(binding_key) + + +class _ModelCache: + + def __init__(self, backend): + self._backend = backend + self._weakrefs = weakref.WeakValueDictionary() + + def get(self, entity_type, *args): + key = (entity_type,) + args + entity = self._weakrefs.get(key) + if entity is None: + entity = entity_type(*args, backend=self._backend, cache=self) + self._weakrefs[key] = entity + return entity + + +class Application: + """Represents a named application in the model. + + This might be your application, or might be an application that you are related to. + Charmers should not instantiate Application objects directly, but should use + :meth:`Model.get_app` if they need a reference to a given application. + + Attributes: + name: The name of this application (eg, 'mysql'). This name may differ from the name of + the charm, if the user has deployed it to a different name. + """ + + def __init__(self, name, backend, cache): + self.name = name + self._backend = backend + self._cache = cache + self._is_our_app = self.name == self._backend.app_name + self._status = None + + def _invalidate(self): + self._status = None + + @property + def status(self) -> 'StatusBase': + """Used to report or read the status of the overall application. + + Can only be read and set by the lead unit of the application. + + The status of remote units is always Unknown. + + Raises: + RuntimeError: if you try to set the status of another application, or if you try to + set the status of this application as a unit that is not the leader. + InvalidStatusError: if you try to set the status to something that is not a + :class:`StatusBase` + + Example:: + + self.model.app.status = BlockedStatus('I need a human to come help me') + """ + if not self._is_our_app: + return UnknownStatus() + + if not self._backend.is_leader(): + raise RuntimeError('cannot get application status as a non-leader unit') + + if self._status: + return self._status + + s = self._backend.status_get(is_app=True) + self._status = StatusBase.from_name(s['status'], s['message']) + return self._status + + @status.setter + def status(self, value: 'StatusBase'): + if not isinstance(value, StatusBase): + raise InvalidStatusError( + 'invalid value provided for application {} status: {}'.format(self, value) + ) + + if not self._is_our_app: + raise RuntimeError('cannot to set status for a remote application {}'.format(self)) + + if not self._backend.is_leader(): + raise RuntimeError('cannot set application status as a non-leader unit') + + self._backend.status_set(value.name, value.message, is_app=True) + self._status = value + + def __repr__(self): + return '<{}.{} {}>'.format(type(self).__module__, type(self).__name__, self.name) + + +class Unit: + """Represents a named unit in the model. + + This might be your unit, another unit of your application, or a unit of another application + that you are related to. + + Attributes: + name: The name of the unit (eg, 'mysql/0') + app: The Application the unit is a part of. + """ + + def __init__(self, name, backend, cache): + self.name = name + + app_name = name.split('/')[0] + self.app = cache.get(Application, app_name) + + self._backend = backend + self._cache = cache + self._is_our_unit = self.name == self._backend.unit_name + self._status = None + + def _invalidate(self): + self._status = None + + @property + def status(self) -> 'StatusBase': + """Used to report or read the status of a specific unit. + + The status of any unit other than yourself is always Unknown. + + Raises: + RuntimeError: if you try to set the status of a unit other than yourself. + InvalidStatusError: if you try to set the status to something other than + a :class:`StatusBase` + Example:: + + self.model.unit.status = MaintenanceStatus('reconfiguring the frobnicators') + """ + if not self._is_our_unit: + return UnknownStatus() + + if self._status: + return self._status + + s = self._backend.status_get(is_app=False) + self._status = StatusBase.from_name(s['status'], s['message']) + return self._status + + @status.setter + def status(self, value: 'StatusBase'): + if not isinstance(value, StatusBase): + raise InvalidStatusError( + 'invalid value provided for unit {} status: {}'.format(self, value) + ) + + if not self._is_our_unit: + raise RuntimeError('cannot set status for a remote unit {}'.format(self)) + + self._backend.status_set(value.name, value.message, is_app=False) + self._status = value + + def __repr__(self): + return '<{}.{} {}>'.format(type(self).__module__, type(self).__name__, self.name) + + def is_leader(self) -> bool: + """Return whether this unit is the leader of its application. + + This can only be called for your own unit. + Returns: + True if you are the leader, False otherwise + Raises: + RuntimeError: if called for a unit that is not yourself + """ + if self._is_our_unit: + # This value is not cached as it is not guaranteed to persist for the whole duration + # of a hook execution. + return self._backend.is_leader() + else: + raise RuntimeError( + 'leadership status of remote units ({}) is not visible to other' + ' applications'.format(self) + ) + + def set_workload_version(self, version: str) -> None: + """Record the version of the software running as the workload. + + This shouldn't be confused with the revision of the charm. This is informative only; + shown in the output of 'juju status'. + """ + if not isinstance(version, str): + raise TypeError("workload version must be a str, not {}: {!r}".format( + type(version).__name__, version)) + self._backend.application_version_set(version) + + +class LazyMapping(Mapping, ABC): + """Represents a dict that isn't populated until it is accessed. + + Charm authors should generally never need to use this directly, but it forms + the basis for many of the dicts that the framework tracks. + """ + + _lazy_data = None + + @abstractmethod + def _load(self): + raise NotImplementedError() + + @property + def _data(self): + data = self._lazy_data + if data is None: + data = self._lazy_data = self._load() + return data + + def _invalidate(self): + self._lazy_data = None + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + +class RelationMapping(Mapping): + """Map of relation names to lists of :class:`Relation` instances.""" + + def __init__(self, relations_meta, our_unit, backend, cache): + self._peers = set() + for name, relation_meta in relations_meta.items(): + if relation_meta.role.is_peer(): + self._peers.add(name) + self._our_unit = our_unit + self._backend = backend + self._cache = cache + self._data = {relation_name: None for relation_name in relations_meta} + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, relation_name): + is_peer = relation_name in self._peers + relation_list = self._data[relation_name] + if relation_list is None: + relation_list = self._data[relation_name] = [] + for rid in self._backend.relation_ids(relation_name): + relation = Relation(relation_name, rid, is_peer, + self._our_unit, self._backend, self._cache) + relation_list.append(relation) + return relation_list + + def _invalidate(self, relation_name): + """Used to wipe the cache of a given relation_name. + + Not meant to be used by Charm authors. The content of relation data is + static for the lifetime of a hook, so it is safe to cache in memory once + accessed. + """ + self._data[relation_name] = None + + def _get_unique(self, relation_name, relation_id=None): + if relation_id is not None: + if not isinstance(relation_id, int): + raise ModelError('relation id {} must be int or None not {}'.format( + relation_id, + type(relation_id).__name__)) + for relation in self[relation_name]: + if relation.id == relation_id: + return relation + else: + # The relation may be dead, but it is not forgotten. + is_peer = relation_name in self._peers + return Relation(relation_name, relation_id, is_peer, + self._our_unit, self._backend, self._cache) + num_related = len(self[relation_name]) + if num_related == 0: + return None + elif num_related == 1: + return self[relation_name][0] + else: + # TODO: We need something in the framework to catch and gracefully handle + # errors, ideally integrating the error catching with Juju's mechanisms. + raise TooManyRelatedAppsError(relation_name, num_related, 1) + + +class BindingMapping: + """Mapping of endpoints to network bindings. + + Charm authors should not instantiate this directly, but access it via + :meth:`Model.get_binding` + """ + + def __init__(self, backend): + self._backend = backend + self._data = {} + + def get(self, binding_key: typing.Union[str, 'Relation']) -> 'Binding': + """Get a specific Binding for an endpoint/relation. + + Not used directly by Charm authors. See :meth:`Model.get_binding` + """ + if isinstance(binding_key, Relation): + binding_name = binding_key.name + relation_id = binding_key.id + elif isinstance(binding_key, str): + binding_name = binding_key + relation_id = None + else: + raise ModelError('binding key must be str or relation instance, not {}' + ''.format(type(binding_key).__name__)) + binding = self._data.get(binding_key) + if binding is None: + binding = Binding(binding_name, relation_id, self._backend) + self._data[binding_key] = binding + return binding + + +class Binding: + """Binding to a network space. + + Attributes: + name: The name of the endpoint this binding represents (eg, 'db') + """ + + def __init__(self, name, relation_id, backend): + self.name = name + self._relation_id = relation_id + self._backend = backend + self._network = None + + @property + def network(self) -> 'Network': + """The network information for this binding.""" + if self._network is None: + try: + self._network = Network(self._backend.network_get(self.name, self._relation_id)) + except RelationNotFoundError: + if self._relation_id is None: + raise + # If a relation is dead, we can still get network info associated with an + # endpoint itself + self._network = Network(self._backend.network_get(self.name)) + return self._network + + +class Network: + """Network space details. + + Charm authors should not instantiate this directly, but should get access to the Network + definition from :meth:`Model.get_binding` and its ``network`` attribute. + + Attributes: + interfaces: A list of :class:`NetworkInterface` details. This includes the + information about how your application should be configured (eg, what + IP addresses should you bind to.) + Note that multiple addresses for a single interface are represented as multiple + interfaces. (eg, ``[NetworKInfo('ens1', '10.1.1.1/32'), + NetworkInfo('ens1', '10.1.2.1/32'])``) + ingress_addresses: A list of :class:`ipaddress.ip_address` objects representing the IP + addresses that other units should use to get in touch with you. + egress_subnets: A list of :class:`ipaddress.ip_network` representing the subnets that + other units will see you connecting from. Due to things like NAT it isn't always + possible to narrow it down to a single address, but when it is clear, the CIDRs + will be constrained to a single address. (eg, 10.0.0.1/32) + Args: + network_info: A dict of network information as returned by ``network-get``. + """ + + def __init__(self, network_info: dict): + self.interfaces = [] + # Treat multiple addresses on an interface as multiple logical + # interfaces with the same name. + for interface_info in network_info['bind-addresses']: + interface_name = interface_info['interface-name'] + for address_info in interface_info['addresses']: + self.interfaces.append(NetworkInterface(interface_name, address_info)) + self.ingress_addresses = [] + for address in network_info['ingress-addresses']: + self.ingress_addresses.append(ipaddress.ip_address(address)) + self.egress_subnets = [] + for subnet in network_info['egress-subnets']: + self.egress_subnets.append(ipaddress.ip_network(subnet)) + + @property + def bind_address(self): + """A single address that your application should bind() to. + + For the common case where there is a single answer. This represents a single + address from :attr:`.interfaces` that can be used to configure where your + application should bind() and listen(). + """ + return self.interfaces[0].address + + @property + def ingress_address(self): + """The address other applications should use to connect to your unit. + + Due to things like public/private addresses, NAT and tunneling, the address you bind() + to is not always the address other people can use to connect() to you. + This is just the first address from :attr:`.ingress_addresses`. + """ + return self.ingress_addresses[0] + + +class NetworkInterface: + """Represents a single network interface that the charm needs to know about. + + Charmers should not instantiate this type directly. Instead use :meth:`Model.get_binding` + to get the network information for a given endpoint. + + Attributes: + name: The name of the interface (eg. 'eth0', or 'ens1') + subnet: An :class:`ipaddress.ip_network` representation of the IP for the network + interface. This may be a single address (eg '10.0.1.2/32') + """ + + def __init__(self, name: str, address_info: dict): + self.name = name + # TODO: expose a hardware address here, see LP: #1864070. + self.address = ipaddress.ip_address(address_info['value']) + cidr = address_info['cidr'] + if not cidr: + # The cidr field may be empty, see LP: #1864102. + # In this case, make it a /32 or /128 IP network. + self.subnet = ipaddress.ip_network(address_info['value']) + else: + self.subnet = ipaddress.ip_network(cidr) + # TODO: expose a hostname/canonical name for the address here, see LP: #1864086. + + +class Relation: + """Represents an established relation between this application and another application. + + This class should not be instantiated directly, instead use :meth:`Model.get_relation` + or :attr:`RelationEvent.relation`. + + Attributes: + name: The name of the local endpoint of the relation (eg 'db') + id: The identifier for a particular relation (integer) + app: An :class:`Application` representing the remote application of this relation. + For peer relations this will be the local application. + units: A set of :class:`Unit` for units that have started and joined this relation. + data: A :class:`RelationData` holding the data buckets for each entity + of a relation. Accessed via eg Relation.data[unit]['foo'] + """ + + def __init__( + self, relation_name: str, relation_id: int, is_peer: bool, our_unit: Unit, + backend: '_ModelBackend', cache: '_ModelCache'): + self.name = relation_name + self.id = relation_id + self.app = None + self.units = set() + + # For peer relations, both the remote and the local app are the same. + if is_peer: + self.app = our_unit.app + try: + for unit_name in backend.relation_list(self.id): + unit = cache.get(Unit, unit_name) + self.units.add(unit) + if self.app is None: + self.app = unit.app + except RelationNotFoundError: + # If the relation is dead, just treat it as if it has no remote units. + pass + self.data = RelationData(self, our_unit, backend) + + def __repr__(self): + return '<{}.{} {}:{}>'.format(type(self).__module__, + type(self).__name__, + self.name, + self.id) + + +class RelationData(Mapping): + """Represents the various data buckets of a given relation. + + Each unit and application involved in a relation has their own data bucket. + Eg: ``{entity: RelationDataContent}`` + where entity can be either a :class:`Unit` or a :class:`Application`. + + Units can read and write their own data, and if they are the leader, + they can read and write their application data. They are allowed to read + remote unit and application data. + + This class should not be created directly. It should be accessed via + :attr:`Relation.data` + """ + + def __init__(self, relation: Relation, our_unit: Unit, backend: '_ModelBackend'): + self.relation = weakref.proxy(relation) + self._data = { + our_unit: RelationDataContent(self.relation, our_unit, backend), + our_unit.app: RelationDataContent(self.relation, our_unit.app, backend), + } + self._data.update({ + unit: RelationDataContent(self.relation, unit, backend) + for unit in self.relation.units}) + # The relation might be dead so avoid a None key here. + if self.relation.app is not None: + self._data.update({ + self.relation.app: RelationDataContent(self.relation, self.relation.app, backend), + }) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + +# We mix in MutableMapping here to get some convenience implementations, but whether it's actually +# mutable or not is controlled by the flag. +class RelationDataContent(LazyMapping, MutableMapping): + + def __init__(self, relation, entity, backend): + self.relation = relation + self._entity = entity + self._backend = backend + self._is_app = isinstance(entity, Application) + + def _load(self): + try: + return self._backend.relation_get(self.relation.id, self._entity.name, self._is_app) + except RelationNotFoundError: + # Dead relations tell no tales (and have no data). + return {} + + def _is_mutable(self): + if self._is_app: + is_our_app = self._backend.app_name == self._entity.name + if not is_our_app: + return False + # Whether the application data bag is mutable or not depends on + # whether this unit is a leader or not, but this is not guaranteed + # to be always true during the same hook execution. + return self._backend.is_leader() + else: + is_our_unit = self._backend.unit_name == self._entity.name + if is_our_unit: + return True + return False + + def __setitem__(self, key, value): + if not self._is_mutable(): + raise RelationDataError('cannot set relation data for {}'.format(self._entity.name)) + if not isinstance(value, str): + raise RelationDataError('relation data values must be strings') + + self._backend.relation_set(self.relation.id, key, value, self._is_app) + + # Don't load data unnecessarily if we're only updating. + if self._lazy_data is not None: + if value == '': + # Match the behavior of Juju, which is that setting the value to an + # empty string will remove the key entirely from the relation data. + del self._data[key] + else: + self._data[key] = value + + def __delitem__(self, key): + # Match the behavior of Juju, which is that setting the value to an empty + # string will remove the key entirely from the relation data. + self.__setitem__(key, '') + + +class ConfigData(LazyMapping): + + def __init__(self, backend): + self._backend = backend + + def _load(self): + return self._backend.config_get() + + +class StatusBase: + """Status values specific to applications and units. + + To access a status by name, see :meth:`StatusBase.from_name`, most use cases will just + directly use the child class to indicate their status. + """ + + _statuses = {} + name = None + + def __init__(self, message: str): + self.message = message + + def __new__(cls, *args, **kwargs): + if cls is StatusBase: + raise TypeError("cannot instantiate a base class") + return super().__new__(cls) + + def __eq__(self, other): + if not isinstance(self, type(other)): + return False + return self.message == other.message + + def __repr__(self): + return "{.__class__.__name__}({!r})".format(self, self.message) + + @classmethod + def from_name(cls, name: str, message: str): + if name == 'unknown': + # unknown is special + return UnknownStatus() + else: + return cls._statuses[name](message) + + @classmethod + def register(cls, child): + if child.name is None: + raise AttributeError('cannot register a Status which has no name') + cls._statuses[child.name] = child + return child + + +@StatusBase.register +class UnknownStatus(StatusBase): + """The unit status is unknown. + + A unit-agent has finished calling install, config-changed and start, but the + charm has not called status-set yet. + + """ + name = 'unknown' + + def __init__(self): + # Unknown status cannot be set and does not have a message associated with it. + super().__init__('') + + def __repr__(self): + return "UnknownStatus()" + + +@StatusBase.register +class ActiveStatus(StatusBase): + """The unit is ready. + + The unit believes it is correctly offering all the services it has been asked to offer. + """ + name = 'active' + + def __init__(self, message: str = ''): + super().__init__(message) + + +@StatusBase.register +class BlockedStatus(StatusBase): + """The unit requires manual intervention. + + An operator has to manually intervene to unblock the unit and let it proceed. + """ + name = 'blocked' + + +@StatusBase.register +class MaintenanceStatus(StatusBase): + """The unit is performing maintenance tasks. + + The unit is not yet providing services, but is actively doing work in preparation + for providing those services. This is a "spinning" state, not an error state. It + reflects activity on the unit itself, not on peers or related units. + + """ + name = 'maintenance' + + +@StatusBase.register +class WaitingStatus(StatusBase): + """A unit is unable to progress. + + The unit is unable to progress to an active state because an application to which + it is related is not running. + + """ + name = 'waiting' + + +class Resources: + """Object representing resources for the charm. + """ + + def __init__(self, names: typing.Iterable[str], backend: '_ModelBackend'): + self._backend = backend + self._paths = {name: None for name in names} + + def fetch(self, name: str) -> Path: + """Fetch the resource from the controller or store. + + If successfully fetched, this returns a Path object to where the resource is stored + on disk, otherwise it raises a ModelError. + """ + if name not in self._paths: + raise RuntimeError('invalid resource name: {}'.format(name)) + if self._paths[name] is None: + self._paths[name] = Path(self._backend.resource_get(name)) + return self._paths[name] + + +class Pod: + """Represents the definition of a pod spec in Kubernetes models. + + Currently only supports simple access to setting the Juju pod spec via :attr:`.set_spec`. + """ + + def __init__(self, backend: '_ModelBackend'): + self._backend = backend + + def set_spec(self, spec: typing.Mapping, k8s_resources: typing.Mapping = None): + """Set the specification for pods that Juju should start in kubernetes. + + See `juju help-tool pod-spec-set` for details of what should be passed. + Args: + spec: The mapping defining the pod specification + k8s_resources: Additional kubernetes specific specification. + + Returns: + """ + if not self._backend.is_leader(): + raise ModelError('cannot set a pod spec as this unit is not a leader') + self._backend.pod_spec_set(spec, k8s_resources) + + +class StorageMapping(Mapping): + """Map of storage names to lists of Storage instances.""" + + def __init__(self, storage_names: typing.Iterable[str], backend: '_ModelBackend'): + self._backend = backend + self._storage_map = {storage_name: None for storage_name in storage_names} + + def __contains__(self, key: str): + return key in self._storage_map + + def __len__(self): + return len(self._storage_map) + + def __iter__(self): + return iter(self._storage_map) + + def __getitem__(self, storage_name: str) -> typing.List['Storage']: + storage_list = self._storage_map[storage_name] + if storage_list is None: + storage_list = self._storage_map[storage_name] = [] + for storage_id in self._backend.storage_list(storage_name): + storage_list.append(Storage(storage_name, storage_id, self._backend)) + return storage_list + + def request(self, storage_name: str, count: int = 1): + """Requests new storage instances of a given name. + + Uses storage-add tool to request additional storage. Juju will notify the unit + via -storage-attached events when it becomes available. + """ + if storage_name not in self._storage_map: + raise ModelError(('cannot add storage {!r}:' + ' it is not present in the charm metadata').format(storage_name)) + self._backend.storage_add(storage_name, count) + + +class Storage: + """"Represents a storage as defined in metadata.yaml + + Attributes: + name: Simple string name of the storage + id: The provider id for storage + """ + + def __init__(self, storage_name, storage_id, backend): + self.name = storage_name + self.id = storage_id + self._backend = backend + self._location = None + + @property + def location(self): + if self._location is None: + raw = self._backend.storage_get('{}/{}'.format(self.name, self.id), "location") + self._location = Path(raw) + return self._location + + +class ModelError(Exception): + """Base class for exceptions raised when interacting with the Model.""" + pass + + +class TooManyRelatedAppsError(ModelError): + """Raised by :meth:`Model.get_relation` if there is more than one related application.""" + + def __init__(self, relation_name, num_related, max_supported): + super().__init__('Too many remote applications on {} ({} > {})'.format( + relation_name, num_related, max_supported)) + self.relation_name = relation_name + self.num_related = num_related + self.max_supported = max_supported + + +class RelationDataError(ModelError): + """Raised by ``Relation.data[entity][key] = 'foo'`` if the data is invalid. + + This is raised if you're either trying to set a value to something that isn't a string, + or if you are trying to set a value in a bucket that you don't have access to. (eg, + another application/unit or setting your application data but you aren't the leader.) + """ + + +class RelationNotFoundError(ModelError): + """Backend error when querying juju for a given relation and that relation doesn't exist.""" + + +class InvalidStatusError(ModelError): + """Raised if trying to set an Application or Unit status to something invalid.""" + + +class _ModelBackend: + """Represents the connection between the Model representation and talking to Juju. + + Charm authors should not directly interact with the ModelBackend, it is a private + implementation of Model. + """ + + LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30) + + def __init__(self, unit_name=None, model_name=None): + if unit_name is None: + self.unit_name = os.environ['JUJU_UNIT_NAME'] + else: + self.unit_name = unit_name + if model_name is None: + model_name = os.environ.get('JUJU_MODEL_NAME') + self.model_name = model_name + self.app_name = self.unit_name.split('/')[0] + + self._is_leader = None + self._leader_check_time = None + + def _run(self, *args, return_output=False, use_json=False): + kwargs = dict(stdout=PIPE, stderr=PIPE) + if use_json: + args += ('--format=json',) + try: + result = run(args, check=True, **kwargs) + except CalledProcessError as e: + raise ModelError(e.stderr) + if return_output: + if result.stdout is None: + return '' + else: + text = result.stdout.decode('utf8') + if use_json: + return json.loads(text) + else: + return text + + def relation_ids(self, relation_name): + relation_ids = self._run('relation-ids', relation_name, return_output=True, use_json=True) + return [int(relation_id.split(':')[-1]) for relation_id in relation_ids] + + def relation_list(self, relation_id): + try: + return self._run('relation-list', '-r', str(relation_id), + return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def relation_get(self, relation_id, member_name, is_app): + if not isinstance(is_app, bool): + raise TypeError('is_app parameter to relation_get must be a boolean') + + if is_app: + version = JujuVersion.from_environ() + if not version.has_app_data(): + raise RuntimeError( + 'getting application data is not supported on Juju version {}'.format(version)) + + args = ['relation-get', '-r', str(relation_id), '-', member_name] + if is_app: + args.append('--app') + + try: + return self._run(*args, return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def relation_set(self, relation_id, key, value, is_app): + if not isinstance(is_app, bool): + raise TypeError('is_app parameter to relation_set must be a boolean') + + if is_app: + version = JujuVersion.from_environ() + if not version.has_app_data(): + raise RuntimeError( + 'setting application data is not supported on Juju version {}'.format(version)) + + args = ['relation-set', '-r', str(relation_id), '{}={}'.format(key, value)] + if is_app: + args.append('--app') + + try: + return self._run(*args) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def config_get(self): + return self._run('config-get', return_output=True, use_json=True) + + def is_leader(self): + """Obtain the current leadership status for the unit the charm code is executing on. + + The value is cached for the duration of a lease which is 30s in Juju. + """ + now = time.monotonic() + if self._leader_check_time is None: + check = True + else: + time_since_check = datetime.timedelta(seconds=now - self._leader_check_time) + check = (time_since_check > self.LEASE_RENEWAL_PERIOD or self._is_leader is None) + if check: + # Current time MUST be saved before running is-leader to ensure the cache + # is only used inside the window that is-leader itself asserts. + self._leader_check_time = now + self._is_leader = self._run('is-leader', return_output=True, use_json=True) + + return self._is_leader + + def resource_get(self, resource_name): + return self._run('resource-get', resource_name, return_output=True).strip() + + def pod_spec_set(self, spec, k8s_resources): + tmpdir = Path(tempfile.mkdtemp('-pod-spec-set')) + try: + spec_path = tmpdir / 'spec.json' + spec_path.write_text(json.dumps(spec)) + args = ['--file', str(spec_path)] + if k8s_resources: + k8s_res_path = tmpdir / 'k8s-resources.json' + k8s_res_path.write_text(json.dumps(k8s_resources)) + args.extend(['--k8s-resources', str(k8s_res_path)]) + self._run('pod-spec-set', *args) + finally: + shutil.rmtree(str(tmpdir)) + + def status_get(self, *, is_app=False): + """Get a status of a unit or an application. + + Args: + is_app: A boolean indicating whether the status should be retrieved for a unit + or an application. + """ + content = self._run( + 'status-get', '--include-data', '--application={}'.format(is_app), + use_json=True, + return_output=True) + # Unit status looks like (in YAML): + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + # Application status looks like (in YAML): + # application-status: + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + # units: + # uo/0: + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + + if is_app: + return {'status': content['application-status']['status'], + 'message': content['application-status']['message']} + else: + return content + + def status_set(self, status, message='', *, is_app=False): + """Set a status of a unit or an application. + + Args: + app: A boolean indicating whether the status should be set for a unit or an + application. + """ + if not isinstance(is_app, bool): + raise TypeError('is_app parameter must be boolean') + return self._run('status-set', '--application={}'.format(is_app), status, message) + + def storage_list(self, name): + return [int(s.split('/')[1]) for s in self._run('storage-list', name, + return_output=True, use_json=True)] + + def storage_get(self, storage_name_id, attribute): + return self._run('storage-get', '-s', storage_name_id, attribute, + return_output=True, use_json=True) + + def storage_add(self, name, count=1): + if not isinstance(count, int) or isinstance(count, bool): + raise TypeError('storage count must be integer, got: {} ({})'.format(count, + type(count))) + self._run('storage-add', '{}={}'.format(name, count)) + + def action_get(self): + return self._run('action-get', return_output=True, use_json=True) + + def action_set(self, results): + self._run('action-set', *["{}={}".format(k, v) for k, v in results.items()]) + + def action_log(self, message): + self._run('action-log', message) + + def action_fail(self, message=''): + self._run('action-fail', message) + + def application_version_set(self, version): + self._run('application-version-set', '--', version) + + def juju_log(self, level, message): + self._run('juju-log', '--log-level', level, message) + + def network_get(self, binding_name, relation_id=None): + """Return network info provided by network-get for a given binding. + + Args: + binding_name: A name of a binding (relation name or extra-binding name). + relation_id: An optional relation id to get network info for. + """ + cmd = ['network-get', binding_name] + if relation_id is not None: + cmd.extend(['-r', str(relation_id)]) + try: + return self._run(*cmd, return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def add_metrics(self, metrics, labels=None): + cmd = ['add-metric'] + + if labels: + label_args = [] + for k, v in labels.items(): + _ModelBackendValidator.validate_metric_label(k) + _ModelBackendValidator.validate_label_value(k, v) + label_args.append('{}={}'.format(k, v)) + cmd.extend(['--labels', ','.join(label_args)]) + + metric_args = [] + for k, v in metrics.items(): + _ModelBackendValidator.validate_metric_key(k) + metric_value = _ModelBackendValidator.format_metric_value(v) + metric_args.append('{}={}'.format(k, metric_value)) + cmd.extend(metric_args) + self._run(*cmd) + + +class _ModelBackendValidator: + """Provides facilities for validating inputs and formatting them for model backends.""" + + METRIC_KEY_REGEX = re.compile(r'^[a-zA-Z](?:[a-zA-Z0-9-_]*[a-zA-Z0-9])?$') + + @classmethod + def validate_metric_key(cls, key): + if cls.METRIC_KEY_REGEX.match(key) is None: + raise ModelError( + 'invalid metric key {!r}: must match {}'.format( + key, cls.METRIC_KEY_REGEX.pattern)) + + @classmethod + def validate_metric_label(cls, label_name): + if cls.METRIC_KEY_REGEX.match(label_name) is None: + raise ModelError( + 'invalid metric label name {!r}: must match {}'.format( + label_name, cls.METRIC_KEY_REGEX.pattern)) + + @classmethod + def format_metric_value(cls, value): + try: + decimal_value = decimal.Decimal.from_float(value) + except TypeError as e: + e2 = ModelError('invalid metric value {!r} provided:' + ' must be a positive finite float'.format(value)) + raise e2 from e + if decimal_value.is_nan() or decimal_value.is_infinite() or decimal_value < 0: + raise ModelError('invalid metric value {!r} provided:' + ' must be a positive finite float'.format(value)) + return str(decimal_value) + + @classmethod + def validate_label_value(cls, label, value): + # Label values cannot be empty, contain commas or equal signs as those are + # used by add-metric as separators. + if not value: + raise ModelError( + 'metric label {} has an empty value, which is not allowed'.format(label)) + v = str(value) + if re.search('[,=]', v) is not None: + raise ModelError( + 'metric label values must not contain "," or "=": {}={!r}'.format(label, value)) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/storage.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/storage.py new file mode 100755 index 00000000..d4310ce1 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/storage.py @@ -0,0 +1,318 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import timedelta +import pickle +import shutil +import subprocess +import sqlite3 +import typing + +import yaml + + +class SQLiteStorage: + + DB_LOCK_TIMEOUT = timedelta(hours=1) + + def __init__(self, filename): + # The isolation_level argument is set to None such that the implicit + # transaction management behavior of the sqlite3 module is disabled. + self._db = sqlite3.connect(str(filename), + isolation_level=None, + timeout=self.DB_LOCK_TIMEOUT.total_seconds()) + self._setup() + + def _setup(self): + # Make sure that the database is locked until the connection is closed, + # not until the transaction ends. + self._db.execute("PRAGMA locking_mode=EXCLUSIVE") + c = self._db.execute("BEGIN") + c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'") + if c.fetchone()[0] == 0: + # Keep in mind what might happen if the process dies somewhere below. + # The system must not be rendered permanently broken by that. + self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)") + self._db.execute(''' + CREATE TABLE notice ( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + event_path TEXT, + observer_path TEXT, + method_name TEXT) + ''') + self._db.commit() + + def close(self): + self._db.close() + + def commit(self): + self._db.commit() + + # There's commit but no rollback. For abort to be supported, we'll need logic that + # can rollback decisions made by third-party code in terms of the internal state + # of objects that have been snapshotted, and hooks to let them know about it and + # take the needed actions to undo their logic until the last snapshot. + # This is doable but will increase significantly the chances for mistakes. + + def save_snapshot(self, handle_path: str, snapshot_data: typing.Any) -> None: + """Part of the Storage API, persist a snapshot data under the given handle. + + Args: + handle_path: The string identifying the snapshot. + snapshot_data: The data to be persisted. (as returned by Object.snapshot()). This + might be a dict/tuple/int, but must only contain 'simple' python types. + """ + # Use pickle for serialization, so the value remains portable. + raw_data = pickle.dumps(snapshot_data) + self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, raw_data)) + + def load_snapshot(self, handle_path: str) -> typing.Any: + """Part of the Storage API, retrieve a snapshot that was previously saved. + + Args: + handle_path: The string identifying the snapshot. + Raises: + NoSnapshotError: if there is no snapshot for the given handle_path. + """ + c = self._db.cursor() + c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,)) + row = c.fetchone() + if row: + return pickle.loads(row[0]) + raise NoSnapshotError(handle_path) + + def drop_snapshot(self, handle_path: str): + """Part of the Storage API, remove a snapshot that was previously saved. + + Dropping a snapshot that doesn't exist is treated as a no-op. + """ + self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,)) + + def list_snapshots(self) -> typing.Generator[str, None, None]: + """Return the name of all snapshots that are currently saved.""" + c = self._db.cursor() + c.execute("SELECT handle FROM snapshot") + while True: + rows = c.fetchmany() + if not rows: + break + for row in rows: + yield row[0] + + def save_notice(self, event_path: str, observer_path: str, method_name: str) -> None: + """Part of the Storage API, record an notice (event and observer)""" + self._db.execute('INSERT INTO notice VALUES (NULL, ?, ?, ?)', + (event_path, observer_path, method_name)) + + def drop_notice(self, event_path: str, observer_path: str, method_name: str) -> None: + """Part of the Storage API, remove a notice that was previously recorded.""" + self._db.execute(''' + DELETE FROM notice + WHERE event_path=? + AND observer_path=? + AND method_name=? + ''', (event_path, observer_path, method_name)) + + def notices(self, event_path: typing.Optional[str]) ->\ + typing.Generator[typing.Tuple[str, str, str], None, None]: + """Part of the Storage API, return all notices that begin with event_path. + + Args: + event_path: If supplied, will only yield events that match event_path. If not + supplied (or None/'') will return all events. + Returns: + Iterable of (event_path, observer_path, method_name) tuples + """ + if event_path: + c = self._db.execute(''' + SELECT event_path, observer_path, method_name + FROM notice + WHERE event_path=? + ORDER BY sequence + ''', (event_path,)) + else: + c = self._db.execute(''' + SELECT event_path, observer_path, method_name + FROM notice + ORDER BY sequence + ''') + while True: + rows = c.fetchmany() + if not rows: + break + for row in rows: + yield tuple(row) + + +class JujuStorage: + """"Storing the content tracked by the Framework in Juju. + + This uses :class:`_JujuStorageBackend` to interact with state-get/state-set + as the way to store state for the framework and for components. + """ + + NOTICE_KEY = "#notices#" + + def __init__(self, backend: '_JujuStorageBackend' = None): + self._backend = backend + if backend is None: + self._backend = _JujuStorageBackend() + + def close(self): + return + + def commit(self): + return + + def save_snapshot(self, handle_path: str, snapshot_data: typing.Any) -> None: + self._backend.set(handle_path, snapshot_data) + + def load_snapshot(self, handle_path): + try: + content = self._backend.get(handle_path) + except KeyError: + raise NoSnapshotError(handle_path) + return content + + def drop_snapshot(self, handle_path): + self._backend.delete(handle_path) + + def save_notice(self, event_path: str, observer_path: str, method_name: str): + notice_list = self._load_notice_list() + notice_list.append([event_path, observer_path, method_name]) + self._save_notice_list(notice_list) + + def drop_notice(self, event_path: str, observer_path: str, method_name: str): + notice_list = self._load_notice_list() + notice_list.remove([event_path, observer_path, method_name]) + self._save_notice_list(notice_list) + + def notices(self, event_path: str): + notice_list = self._load_notice_list() + for row in notice_list: + if row[0] != event_path: + continue + yield tuple(row) + + def _load_notice_list(self) -> typing.List[typing.Tuple[str]]: + try: + notice_list = self._backend.get(self.NOTICE_KEY) + except KeyError: + return [] + if notice_list is None: + return [] + return notice_list + + def _save_notice_list(self, notices: typing.List[typing.Tuple[str]]) -> None: + self._backend.set(self.NOTICE_KEY, notices) + + +class _SimpleLoader(getattr(yaml, 'CSafeLoader', yaml.SafeLoader)): + """Handle a couple basic python types. + + yaml.SafeLoader can handle all the basic int/float/dict/set/etc that we want. The only one + that it *doesn't* handle is tuples. We don't want to support arbitrary types, so we just + subclass SafeLoader and add tuples back in. + """ + # Taken from the example at: + # https://stackoverflow.com/questions/9169025/how-can-i-add-a-python-tuple-to-a-yaml-file-using-pyyaml + + construct_python_tuple = yaml.Loader.construct_python_tuple + + +_SimpleLoader.add_constructor( + u'tag:yaml.org,2002:python/tuple', + _SimpleLoader.construct_python_tuple) + + +class _SimpleDumper(getattr(yaml, 'CSafeDumper', yaml.SafeDumper)): + """Add types supported by 'marshal' + + YAML can support arbitrary types, but that is generally considered unsafe (like pickle). So + we want to only support dumping out types that are safe to load. + """ + + +_SimpleDumper.represent_tuple = yaml.Dumper.represent_tuple +_SimpleDumper.add_representer(tuple, _SimpleDumper.represent_tuple) + + +class _JujuStorageBackend: + """Implements the interface from the Operator framework to Juju's state-get/set/etc.""" + + @staticmethod + def is_available() -> bool: + """Check if Juju state storage is available. + + This checks if there is a 'state-get' executable in PATH. + """ + p = shutil.which('state-get') + return p is not None + + def set(self, key: str, value: typing.Any) -> None: + """Set a key to a given value. + + Args: + key: The string key that will be used to find the value later + value: Arbitrary content that will be returned by get(). + Raises: + CalledProcessError: if 'state-set' returns an error code. + """ + # default_flow_style=None means that it can use Block for + # complex types (types that have nested types) but use flow + # for simple types (like an array). Not all versions of PyYAML + # have the same default style. + encoded_value = yaml.dump(value, Dumper=_SimpleDumper, default_flow_style=None) + content = yaml.dump( + {key: encoded_value}, encoding='utf-8', default_style='|', + default_flow_style=False, + Dumper=_SimpleDumper) + subprocess.run(["state-set", "--file", "-"], input=content, check=True) + + def get(self, key: str) -> typing.Any: + """Get the bytes value associated with a given key. + + Args: + key: The string key that will be used to find the value + Raises: + CalledProcessError: if 'state-get' returns an error code. + """ + # We don't capture stderr here so it can end up in debug logs. + p = subprocess.run( + ["state-get", key], + stdout=subprocess.PIPE, + check=True, + ) + if p.stdout == b'' or p.stdout == b'\n': + raise KeyError(key) + return yaml.load(p.stdout, Loader=_SimpleLoader) + + def delete(self, key: str) -> None: + """Remove a key from being tracked. + + Args: + key: The key to stop storing + Raises: + CalledProcessError: if 'state-delete' returns an error code. + """ + subprocess.run(["state-delete", key], check=True) + + +class NoSnapshotError(Exception): + + def __init__(self, handle_path): + self.handle_path = handle_path + + def __str__(self): + return 'no snapshot data found for {} object'.format(self.handle_path) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/testing.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/testing.py new file mode 100755 index 00000000..b4b3fe07 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/testing.py @@ -0,0 +1,586 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import pathlib +from textwrap import dedent +import tempfile +import typing +import yaml +import weakref + +from ops import ( + charm, + framework, + model, + storage, +) + + +# OptionalYAML is something like metadata.yaml or actions.yaml. You can +# pass in a file-like object or the string directly. +OptionalYAML = typing.Optional[typing.Union[str, typing.TextIO]] + + +# noinspection PyProtectedMember +class Harness: + """This class represents a way to build up the model that will drive a test suite. + + The model that is created is from the viewpoint of the charm that you are testing. + + Example:: + + harness = Harness(MyCharm) + # Do initial setup here + relation_id = harness.add_relation('db', 'postgresql') + # Now instantiate the charm to see events as the model changes + harness.begin() + harness.add_relation_unit(relation_id, 'postgresql/0') + harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'}) + # Check that charm has properly handled the relation_joined event for postgresql/0 + self.assertEqual(harness.charm. ...) + + Args: + charm_cls: The Charm class that you'll be testing. + meta: charm.CharmBase is a A string or file-like object containing the contents of + metadata.yaml. If not supplied, we will look for a 'metadata.yaml' file in the + parent directory of the Charm, and if not found fall back to a trivial + 'name: test-charm' metadata. + actions: A string or file-like object containing the contents of + actions.yaml. If not supplied, we will look for a 'actions.yaml' file in the + parent directory of the Charm. + """ + + def __init__( + self, + charm_cls: typing.Type[charm.CharmBase], + *, + meta: OptionalYAML = None, + actions: OptionalYAML = None): + # TODO: jam 2020-03-05 We probably want to take config as a parameter as well, since + # it would define the default values of config that the charm would see. + self._charm_cls = charm_cls + self._charm = None + self._charm_dir = 'no-disk-path' # this may be updated by _create_meta + self._lazy_resource_dir = None + self._meta = self._create_meta(meta, actions) + self._unit_name = self._meta.name + '/0' + self._framework = None + self._hooks_enabled = True + self._relation_id_counter = 0 + self._backend = _TestingModelBackend(self._unit_name, self._meta) + self._model = model.Model(self._meta, self._backend) + self._storage = storage.SQLiteStorage(':memory:') + self._framework = framework.Framework( + self._storage, self._charm_dir, self._meta, self._model) + + @property + def charm(self) -> charm.CharmBase: + """Return the instance of the charm class that was passed to __init__. + + Note that the Charm is not instantiated until you have called + :meth:`.begin()`. + """ + return self._charm + + @property + def model(self) -> model.Model: + """Return the :class:`~ops.model.Model` that is being driven by this Harness.""" + return self._model + + @property + def framework(self) -> framework.Framework: + """Return the Framework that is being driven by this Harness.""" + return self._framework + + @property + def _resource_dir(self) -> pathlib.Path: + if self._lazy_resource_dir is not None: + return self._lazy_resource_dir + + self.__resource_dir = tempfile.TemporaryDirectory() + self._lazy_resource_dir = pathlib.Path(self.__resource_dir.name) + self._finalizer = weakref.finalize(self, self.__resource_dir.cleanup) + return self._lazy_resource_dir + + def begin(self) -> None: + """Instantiate the Charm and start handling events. + + Before calling begin(), there is no Charm instance, so changes to the Model won't emit + events. You must call begin before :attr:`.charm` is valid. + """ + if self._charm is not None: + raise RuntimeError('cannot call the begin method on the harness more than once') + + # The Framework adds attributes to class objects for events, etc. As such, we can't re-use + # the original class against multiple Frameworks. So create a locally defined class + # and register it. + # TODO: jam 2020-03-16 We are looking to changes this to Instance attributes instead of + # Class attributes which should clean up this ugliness. The API can stay the same + class TestEvents(self._charm_cls.on.__class__): + pass + + TestEvents.__name__ = self._charm_cls.on.__class__.__name__ + + class TestCharm(self._charm_cls): + on = TestEvents() + + # Note: jam 2020-03-01 This is so that errors in testing say MyCharm has no attribute foo, + # rather than TestCharm has no attribute foo. + TestCharm.__name__ = self._charm_cls.__name__ + self._charm = TestCharm(self._framework) + + def _create_meta(self, charm_metadata, action_metadata): + """Create a CharmMeta object. + + Handle the cases where a user doesn't supply explicit metadata snippets. + """ + filename = inspect.getfile(self._charm_cls) + charm_dir = pathlib.Path(filename).parents[1] + + if charm_metadata is None: + metadata_path = charm_dir / 'metadata.yaml' + if metadata_path.is_file(): + charm_metadata = metadata_path.read_text() + self._charm_dir = charm_dir + else: + # The simplest of metadata that the framework can support + charm_metadata = 'name: test-charm' + elif isinstance(charm_metadata, str): + charm_metadata = dedent(charm_metadata) + + if action_metadata is None: + actions_path = charm_dir / 'actions.yaml' + if actions_path.is_file(): + action_metadata = actions_path.read_text() + self._charm_dir = charm_dir + elif isinstance(action_metadata, str): + action_metadata = dedent(action_metadata) + + return charm.CharmMeta.from_yaml(charm_metadata, action_metadata) + + def add_oci_resource(self, resource_name: str, + contents: typing.Mapping[str, str] = None) -> None: + """Add oci resources to the backend. + + This will register an oci resource and create a temporary file for processing metadata + about the resource. A default set of values will be used for all the file contents + unless a specific contents dict is provided. + + Args: + resource_name: Name of the resource to add custom contents to. + contents: Optional custom dict to write for the named resource. + """ + if not contents: + contents = {'registrypath': 'registrypath', + 'username': 'username', + 'password': 'password', + } + if resource_name not in self._meta.resources.keys(): + raise RuntimeError('Resource {} is not a defined resources'.format(resource_name)) + if self._meta.resources[resource_name].type != "oci-image": + raise RuntimeError('Resource {} is not an OCI Image'.format(resource_name)) + resource_dir = self._resource_dir / resource_name + resource_dir.mkdir(exist_ok=True) + resource_file = resource_dir / "contents.yaml" + with resource_file.open('wt', encoding='utf8') as resource_yaml: + yaml.dump(contents, resource_yaml) + self._backend._resources_map[resource_name] = resource_file + + def populate_oci_resources(self) -> None: + """Populate all OCI resources.""" + for name, data in self._meta.resources.items(): + if data.type == "oci-image": + self.add_oci_resource(name) + + def disable_hooks(self) -> None: + """Stop emitting hook events when the model changes. + + This can be used by developers to stop changes to the model from emitting events that + the charm will react to. Call :meth:`.enable_hooks` + to re-enable them. + """ + self._hooks_enabled = False + + def enable_hooks(self) -> None: + """Re-enable hook events from charm.on when the model is changed. + + By default hook events are enabled once you call :meth:`.begin`, + but if you have used :meth:`.disable_hooks`, this can be used to + enable them again. + """ + self._hooks_enabled = True + + def _next_relation_id(self): + rel_id = self._relation_id_counter + self._relation_id_counter += 1 + return rel_id + + def add_relation(self, relation_name: str, remote_app: str) -> int: + """Declare that there is a new relation between this app and `remote_app`. + + Args: + relation_name: The relation on Charm that is being related to + remote_app: The name of the application that is being related to + + Return: + The relation_id created by this add_relation. + """ + rel_id = self._next_relation_id() + self._backend._relation_ids_map.setdefault(relation_name, []).append(rel_id) + self._backend._relation_names[rel_id] = relation_name + self._backend._relation_list_map[rel_id] = [] + self._backend._relation_data[rel_id] = { + remote_app: {}, + self._backend.unit_name: {}, + self._backend.app_name: {}, + } + # Reload the relation_ids list + if self._model is not None: + self._model.relations._invalidate(relation_name) + if self._charm is None or not self._hooks_enabled: + return rel_id + relation = self._model.get_relation(relation_name, rel_id) + app = self._model.get_app(remote_app) + self._charm.on[relation_name].relation_created.emit( + relation, app) + return rel_id + + def add_relation_unit(self, relation_id: int, remote_unit_name: str) -> None: + """Add a new unit to a relation. + + Example:: + + rel_id = harness.add_relation('db', 'postgresql') + harness.add_relation_unit(rel_id, 'postgresql/0') + + This will trigger a `relation_joined` event and a `relation_changed` event. + + Args: + relation_id: The integer relation identifier (as returned by add_relation). + remote_unit_name: A string representing the remote unit that is being added. + Return: + None + """ + self._backend._relation_list_map[relation_id].append(remote_unit_name) + self._backend._relation_data[relation_id][remote_unit_name] = {} + relation_name = self._backend._relation_names[relation_id] + # Make sure that the Model reloads the relation_list for this relation_id, as well as + # reloading the relation data for this unit. + if self._model is not None: + remote_unit = self._model.get_unit(remote_unit_name) + relation = self._model.get_relation(relation_name, relation_id) + unit_cache = relation.data.get(remote_unit, None) + if unit_cache is not None: + unit_cache._invalidate() + self._model.relations._invalidate(relation_name) + if self._charm is None or not self._hooks_enabled: + return + self._charm.on[relation_name].relation_joined.emit( + relation, remote_unit.app, remote_unit) + + def get_relation_data(self, relation_id: int, app_or_unit: str) -> typing.Mapping: + """Get the relation data bucket for a single app or unit in a given relation. + + This ignores all of the safety checks of who can and can't see data in relations (eg, + non-leaders can't read their own application's relation data because there are no events + that keep that data up-to-date for the unit). + + Args: + relation_id: The relation whose content we want to look at. + app_or_unit: The name of the application or unit whose data we want to read + Return: + a dict containing the relation data for `app_or_unit` or None. + Raises: + KeyError: if relation_id doesn't exist + """ + return self._backend._relation_data[relation_id].get(app_or_unit, None) + + def get_workload_version(self) -> str: + """Read the workload version that was set by the unit.""" + return self._backend._workload_version + + def set_model_name(self, name: str) -> None: + """Set the name of the Model that this is representing. + + This cannot be called once begin() has been called. But it lets you set the value that + will be returned by Model.name. + """ + if self._charm is not None: + raise RuntimeError('cannot set the Model name after begin()') + self._backend.model_name = name + + def update_relation_data( + self, + relation_id: int, + app_or_unit: str, + key_values: typing.Mapping, + ) -> None: + """Update the relation data for a given unit or application in a given relation. + + This also triggers the `relation_changed` event for this relation_id. + + Args: + relation_id: The integer relation_id representing this relation. + app_or_unit: The unit or application name that is being updated. + This can be the local or remote application. + key_values: Each key/value will be updated in the relation data. + """ + relation_name = self._backend._relation_names[relation_id] + relation = self._model.get_relation(relation_name, relation_id) + if '/' in app_or_unit: + entity = self._model.get_unit(app_or_unit) + else: + entity = self._model.get_app(app_or_unit) + rel_data = relation.data.get(entity, None) + if rel_data is not None: + # rel_data may have cached now-stale data, so _invalidate() it. + # Note, this won't cause the data to be loaded if it wasn't already. + rel_data._invalidate() + + new_values = self._backend._relation_data[relation_id][app_or_unit].copy() + for k, v in key_values.items(): + if v == '': + new_values.pop(k, None) + else: + new_values[k] = v + self._backend._relation_data[relation_id][app_or_unit] = new_values + + if app_or_unit == self._model.unit.name: + # No events for our own unit + return + if app_or_unit == self._model.app.name: + # updating our own app only generates an event if it is a peer relation and we + # aren't the leader + is_peer = self._meta.relations[relation_name].role.is_peer() + if not is_peer: + return + if self._model.unit.is_leader(): + return + self._emit_relation_changed(relation_id, app_or_unit) + + def _emit_relation_changed(self, relation_id, app_or_unit): + if self._charm is None or not self._hooks_enabled: + return + rel_name = self._backend._relation_names[relation_id] + relation = self.model.get_relation(rel_name, relation_id) + if '/' in app_or_unit: + app_name = app_or_unit.split('/')[0] + unit_name = app_or_unit + app = self.model.get_app(app_name) + unit = self.model.get_unit(unit_name) + args = (relation, app, unit) + else: + app_name = app_or_unit + app = self.model.get_app(app_name) + args = (relation, app) + self._charm.on[rel_name].relation_changed.emit(*args) + + def update_config( + self, + key_values: typing.Mapping[str, str] = None, + unset: typing.Iterable[str] = (), + ) -> None: + """Update the config as seen by the charm. + + This will trigger a `config_changed` event. + + Args: + key_values: A Mapping of key:value pairs to update in config. + unset: An iterable of keys to remove from Config. (Note that this does + not currently reset the config values to the default defined in config.yaml.) + """ + config = self._backend._config + if key_values is not None: + for key, value in key_values.items(): + config[key] = value + for key in unset: + config.pop(key, None) + # NOTE: jam 2020-03-01 Note that this sort of works "by accident". Config + # is a LazyMapping, but its _load returns a dict and this method mutates + # the dict that Config is caching. Arguably we should be doing some sort + # of charm.framework.model.config._invalidate() + if self._charm is None or not self._hooks_enabled: + return + self._charm.on.config_changed.emit() + + def set_leader(self, is_leader: bool = True) -> None: + """Set whether this unit is the leader or not. + + If this charm becomes a leader then `leader_elected` will be triggered. + + Args: + is_leader: True/False as to whether this unit is the leader. + """ + was_leader = self._backend._is_leader + self._backend._is_leader = is_leader + # Note: jam 2020-03-01 currently is_leader is cached at the ModelBackend level, not in + # the Model objects, so this automatically gets noticed. + if is_leader and not was_leader and self._charm is not None and self._hooks_enabled: + self._charm.on.leader_elected.emit() + + def _get_backend_calls(self, reset: bool = True) -> list: + """Return the calls that we have made to the TestingModelBackend. + + This is useful mostly for testing the framework itself, so that we can assert that we + do/don't trigger extra calls. + + Args: + reset: If True, reset the calls list back to empty, if false, the call list is + preserved. + Return: + ``[(call1, args...), (call2, args...)]`` + """ + calls = self._backend._calls.copy() + if reset: + self._backend._calls.clear() + return calls + + +def _record_calls(cls): + """Replace methods on cls with methods that record that they have been called. + + Iterate all attributes of cls, and for public methods, replace them with a wrapped method + that records the method called along with the arguments and keyword arguments. + """ + for meth_name, orig_method in cls.__dict__.items(): + if meth_name.startswith('_'): + continue + + def decorator(orig_method): + def wrapped(self, *args, **kwargs): + full_args = (orig_method.__name__,) + args + if kwargs: + full_args = full_args + (kwargs,) + self._calls.append(full_args) + return orig_method(self, *args, **kwargs) + return wrapped + + setattr(cls, meth_name, decorator(orig_method)) + return cls + + +@_record_calls +class _TestingModelBackend: + """This conforms to the interface for ModelBackend but provides canned data. + + DO NOT use this class directly, it is used by `Harness`_ to drive the model. + `Harness`_ is responsible for maintaining the internal consistency of the values here, + as the only public methods of this type are for implementing ModelBackend. + """ + + def __init__(self, unit_name, meta): + self.unit_name = unit_name + self.app_name = self.unit_name.split('/')[0] + self.model_name = None + self._calls = [] + self._meta = meta + self._is_leader = None + self._relation_ids_map = {} # relation name to [relation_ids,...] + self._relation_names = {} # reverse map from relation_id to relation_name + self._relation_list_map = {} # relation_id: [unit_name,...] + self._relation_data = {} # {relation_id: {name: data}} + self._config = {} + self._is_leader = False + self._resources_map = {} + self._pod_spec = None + self._app_status = {'status': 'unknown', 'message': ''} + self._unit_status = {'status': 'maintenance', 'message': ''} + self._workload_version = None + + def relation_ids(self, relation_name): + try: + return self._relation_ids_map[relation_name] + except KeyError as e: + if relation_name not in self._meta.relations: + raise model.ModelError('{} is not a known relation'.format(relation_name)) from e + return [] + + def relation_list(self, relation_id): + try: + return self._relation_list_map[relation_id] + except KeyError as e: + raise model.RelationNotFoundError from e + + def relation_get(self, relation_id, member_name, is_app): + if is_app and '/' in member_name: + member_name = member_name.split('/')[0] + if relation_id not in self._relation_data: + raise model.RelationNotFoundError() + return self._relation_data[relation_id][member_name].copy() + + def relation_set(self, relation_id, key, value, is_app): + relation = self._relation_data[relation_id] + if is_app: + bucket_key = self.app_name + else: + bucket_key = self.unit_name + if bucket_key not in relation: + relation[bucket_key] = {} + bucket = relation[bucket_key] + if value == '': + bucket.pop(key, None) + else: + bucket[key] = value + + def config_get(self): + return self._config + + def is_leader(self): + return self._is_leader + + def application_version_set(self, version): + self._workload_version = version + + def resource_get(self, resource_name): + return self._resources_map[resource_name] + + def pod_spec_set(self, spec, k8s_resources): + self._pod_spec = (spec, k8s_resources) + + def status_get(self, *, is_app=False): + if is_app: + return self._app_status + else: + return self._unit_status + + def status_set(self, status, message='', *, is_app=False): + if is_app: + self._app_status = {'status': status, 'message': message} + else: + self._unit_status = {'status': status, 'message': message} + + def storage_list(self, name): + raise NotImplementedError(self.storage_list) + + def storage_get(self, storage_name_id, attribute): + raise NotImplementedError(self.storage_get) + + def storage_add(self, name, count=1): + raise NotImplementedError(self.storage_add) + + def action_get(self): + raise NotImplementedError(self.action_get) + + def action_set(self, results): + raise NotImplementedError(self.action_set) + + def action_log(self, message): + raise NotImplementedError(self.action_log) + + def action_fail(self, message=''): + raise NotImplementedError(self.action_fail) + + def network_get(self, endpoint_name, relation_id=None): + raise NotImplementedError(self.network_get) diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/version.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/version.py new file mode 100644 index 00000000..15e54785 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/lib/ops/version.py @@ -0,0 +1,50 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess +from pathlib import Path + +__all__ = ('version',) + +_FALLBACK = '0.8' # this gets bumped after release + + +def _get_version(): + version = _FALLBACK + ".dev0+unknown" + + p = Path(__file__).parent + if (p.parent / '.git').exists(): + try: + proc = subprocess.run( + ['git', 'describe', '--tags', '--dirty'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + cwd=p, + check=True) + except Exception: + pass + else: + version = proc.stdout.strip().decode('utf8') + if '-' in version: + # version will look like -<#commits>-g[-dirty] + # in terms of PEP 440, the tag we'll make sure is a 'public version identifier'; + # everything after the first - needs to be a 'local version' + public, local = version.split('-', 1) + version = public + '+' + local.replace('-', '.') + # version now +<#commits>.g[.dirty] + # which is PEP440-compliant (as long as is :-) + return version + + +version = _get_version() diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/metadata.yaml b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/metadata.yaml new file mode 100644 index 00000000..4b5b3528 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/metadata.yaml @@ -0,0 +1,28 @@ +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +name: simple-k8s-proxy +summary: A simple example proxy charm +description: | + Simple proxy charm is an example charm used in OSM Hackfests +series: + - kubernetes +peers: + proxypeer: + interface: proxypeer +deployment: + mode: operator \ No newline at end of file diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm new file mode 160000 index 00000000..a7c5b6a8 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/charms.osm @@ -0,0 +1 @@ +Subproject commit a7c5b6a8af22d715276125afc3ed26f0436c71af diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator new file mode 160000 index 00000000..824aa2d8 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/mod/operator @@ -0,0 +1 @@ +Subproject commit 824aa2d8996ea548c913317c2df6bac258f0737b diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/src/charm.py b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/src/charm.py new file mode 100755 index 00000000..e23b12b7 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/charms/simple/src/charm.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +## +# Copyright 2020 Canonical Ltd. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +## + +import sys + +sys.path.append("lib") + +from charms.osm.sshproxy import SSHProxyCharm +from ops.main import main + +class MySSHProxyCharm(SSHProxyCharm): + + def __init__(self, framework, key): + super().__init__(framework, key) + + # Listen to charm events + self.framework.observe(self.on.config_changed, self.on_config_changed) + self.framework.observe(self.on.install, self.on_install) + self.framework.observe(self.on.start, self.on_start) + + # Listen to the touch action event + self.framework.observe(self.on.touch_action, self.on_touch_action) + + def on_config_changed(self, event): + """Handle changes in configuration""" + super().on_config_changed(event) + + def on_install(self, event): + """Called when the charm is being installed""" + super().on_install(event) + + def on_start(self, event): + """Called when the charm is being started""" + super().on_start(event) + + def on_touch_action(self, event): + """Touch a file.""" + + if self.model.unit.is_leader(): + filename = event.params["filename"] + proxy = self.get_ssh_proxy() + stdout, stderr = proxy.run("touch {}".format(filename)) + event.set_results({"output": stdout}) + else: + event.fail("Unit is not leader") + return + +if __name__ == "__main__": + main(MySSHProxyCharm) + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/cloud_init/cloud-config.txt b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/cloud_init/cloud-config.txt new file mode 100755 index 00000000..36c8d1bf --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/Scripts/cloud_init/cloud-config.txt @@ -0,0 +1,12 @@ +#cloud-config +password: osm4u +chpasswd: { expire: False } +ssh_pwauth: True + +write_files: +- content: | + # My new helloworld file + + owner: root:root + permissions: '0644' + path: /root/helloworld.txt diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.mf b/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.mf new file mode 100644 index 00000000..1a151692 --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.mf @@ -0,0 +1,362 @@ + +vnfd_id: k8s_proxy_charm-vnf +vnfd_product_name: k8s_proxy_charm-vnf +vnfd_provider_id: OSM +vnfd_software_version: 1.0 +vnfd_package_version: 1.0.0 +vnfd_release_date_time: 2021-11-09T17:51:08.722637-03:00 +compatible_specification_versions: 3.3.1 +vnfm_info: OSM + +Source: Scripts/cloud_init/cloud-config.txt +Algorithm: SHA-512 +Hash: 66f0a0a5c9e0acbcfb4baa751c2b380bfd99a4a22ca2989b3fb289171961861128eab2325bdc289f2095daf87564d7d5ad888cb1916dc946d02952d900ca9346 + +Source: Scripts/charms/simple/config.yaml +Algorithm: SHA-512 +Hash: 5a50c9de8cfabff0d061fc4dd57ff0892d82eac6b58fa0972b174fb22aeac60d310dd9318fecbc2afbe5e51d2dde94b9ffacdd5d04c46b24f353ca79baa7bffd + +Source: Scripts/charms/simple/actions.yaml +Algorithm: SHA-512 +Hash: d94ef395698e59cab0789643bf754211f783778b5cbb22aa8cd19ac8445952bd14d816accc9635348d9ab8bed9be14c0150534f8580bac17e7fe7bce2230d526 + +Source: Scripts/charms/simple/metadata.yaml +Algorithm: SHA-512 +Hash: 4d23412b57dcf3bb6adc1d8550583698a265503af3688f2461569ef33fc288f3d2ef7c544b2bd8363e4bc3e97a41c641629f14d94073f94da60e736150fcd9ac + +Source: Scripts/charms/simple/src/charm.py +Algorithm: SHA-512 +Hash: cc46ad3d5f13327ae75d1a069bd9aa2cef00c3c7111a80d92b85c3805c002d49a612da1c7459e57286e754344dca0f760c55f36d94d28c3e86cde7ca61a8403a + +Source: Scripts/charms/simple/lib/charms/osm/proxy_cluster.py +Algorithm: SHA-512 +Hash: 9a43040f5d96ee906f4878f9ae0803e85ea708d67dc3bf5655f89d12cb124fc7d7c32d0be6bedec7dffb20355a55ed501dc76321f8ccd3912ade2ef5b4e381c1 + +Source: Scripts/charms/simple/lib/charms/osm/sshproxy.py +Algorithm: SHA-512 +Hash: 5dca5f5c326c3ed2c945d734566370399e470fa42ce51c73ca10a32de8ef97878a68a5434769a5e1637d26c4de624041c1b65a2981e3ac5a44beec0c6dc55138 + +Source: Scripts/charms/simple/lib/charms/osm/libansible.py +Algorithm: SHA-512 +Hash: f3ee2514ad6100f35898b52ccb7b0ba928d8e510c9deff9ad688b5daed4330fb7c9c6d2bf60ffa197da7e7c53af58764cf972e46c37357114464a56bf9637f82 + +Source: Scripts/charms/simple/lib/charms/osm/ns.py +Algorithm: SHA-512 +Hash: 8ceaeb49c67a7389a418580684bb5b7a168d5c2a1b6f0a0f5f0bc9b1346e4b5e56cdd2548036357dd3e5d119cb0af305ac1becaf4e1e8f6abf55718b74b8e628 + +Source: Scripts/charms/simple/lib/ops/charm.py +Algorithm: SHA-512 +Hash: 72bab681b72a29ab7649acb3e6abb9d117c103bb944ebbbdbef1a4b58c997670a305051c05f98fa617f62188d17356b81fcef4a97e0ce9ed6247daf6ac586241 + +Source: Scripts/charms/simple/lib/ops/model.py +Algorithm: SHA-512 +Hash: 2d4098235b1c12a1de8b256fe6ed14d4b21e5bfd0464c56e4c430be20e1c85dd70a624906693a5cbb1ba5e8ba1dce5cea7905d3150166976fee1e9fe780e8f50 + +Source: Scripts/charms/simple/lib/ops/main.py +Algorithm: SHA-512 +Hash: 20eae0c37fab197efcc59a97ed62b378fff0776193910477887b220a13ef11ba98fc059e247390e503f17cd2bcf71e106f6296e5bc02ecd274295ade14da905f + +Source: Scripts/charms/simple/lib/ops/__init__.py +Algorithm: SHA-512 +Hash: 6756076d4b3ab0a9f47c5fd6b06088037749dc0d96075186cb8dc0770fbf2b528a3e69d9364c7e3843efbaafc5ff4c156dbb198a3649692fb2e9134101e6fde7 + +Source: Scripts/charms/simple/lib/ops/log.py +Algorithm: SHA-512 +Hash: b078a1df98276c1bd52ae7ef035912611f176dce4902d6f73aaae1d0527193673ddfbb03a7cad81d98dbb3ca29a7acac317d68a2b663483d20494bb5d9a0641b + +Source: Scripts/charms/simple/lib/ops/framework.py +Algorithm: SHA-512 +Hash: ea92ee16fa046312ac4b684d3bc2e6e7be8600ac67854ce5837284299ef0e5787a2d9770942a3ddf76141e767aab2cc8c4da311ff2c6df69b59fd9e087139240 + +Source: Scripts/charms/simple/lib/ops/jujuversion.py +Algorithm: SHA-512 +Hash: 03d0306f1650e522012c96b7f9ed6770ca17e8fb6a3c7a715c6dfbb9c1a86caae640419dfde053af6675dfad6463ed2a97c8d32215328df4906f496aeba56c46 + +Source: Scripts/charms/simple/lib/ops/testing.py +Algorithm: SHA-512 +Hash: 9b5adebe447c8f2b93a5cd2b6fc65a9582c07260fb8390924ba74bc6b90d296f7eca4caa899dcf62356821dd9321287f99dfe8da9673296c5ed483ce61284769 + +Source: Scripts/charms/simple/lib/ops/storage.py +Algorithm: SHA-512 +Hash: a9e6376d721cca8e8239ce54a8b42cfd48ae91a21f4ff770d77f349894f7bb1bc737221923b3e15e893b95aa4f88166d8eefd57724cbb361c42ca567939a7496 + +Source: Scripts/charms/simple/lib/ops/version.py +Algorithm: SHA-512 +Hash: 340e7e1106caaf0ab155beb1e3daf16a775bc089b137d39adca3450a831e1adc6432b8dc65a3e15ab1d45ba8a8ceabb2cb3d7b16b1705fb68d77a1336aaabd91 + +Source: Scripts/charms/simple/lib/ops/lib/__init__.py +Algorithm: SHA-512 +Hash: 5dd21c89546ce53b877d3a75d2148d877b95f5116beb6f6031e509fa3a843ea9a1df85e407abc6651dbb173c0a67a1f84342aae1c2e49ea433f8385325f01988 + +Source: Scripts/charms/simple/hooks/upgrade-charm +Algorithm: SHA-512 +Hash: cc46ad3d5f13327ae75d1a069bd9aa2cef00c3c7111a80d92b85c3805c002d49a612da1c7459e57286e754344dca0f760c55f36d94d28c3e86cde7ca61a8403a + +Source: Scripts/charms/simple/hooks/start +Algorithm: SHA-512 +Hash: cc46ad3d5f13327ae75d1a069bd9aa2cef00c3c7111a80d92b85c3805c002d49a612da1c7459e57286e754344dca0f760c55f36d94d28c3e86cde7ca61a8403a + +Source: Scripts/charms/simple/hooks/install +Algorithm: SHA-512 +Hash: cc46ad3d5f13327ae75d1a069bd9aa2cef00c3c7111a80d92b85c3805c002d49a612da1c7459e57286e754344dca0f760c55f36d94d28c3e86cde7ca61a8403a + +Source: Scripts/charms/simple/mod/charms.osm/LICENSE +Algorithm: SHA-512 +Hash: dc6b68d13b8cf959644b935f1192b02c71aa7a5cf653bd43b4480fa89eec8d4d3f16a2278ec8c3b40ab1fdb233b3173a78fd83590d6f739e0c9e8ff56c282557 + +Source: Scripts/charms/simple/mod/charms.osm/.git +Algorithm: SHA-512 +Hash: c6f622ec92a2d4b1a3a5ae171d836c3803e375276335dda4d218ccd10c6fd8287e9b0df9b2d70c06d262003c26161c1b5f952fdc31cae2a1821c642e209522dd + +Source: Scripts/charms/simple/mod/charms.osm/README.md +Algorithm: SHA-512 +Hash: 9b59327308b486a924299368b61667f64a0ba3d6974bc0a607c7ffd065f9e3c4da08e0079b735de1e58df012ab1fcf9bfcc1750b0b279df53265eb73773b8854 + +Source: Scripts/charms/simple/mod/charms.osm/charms/osm/proxy_cluster.py +Algorithm: SHA-512 +Hash: 9a43040f5d96ee906f4878f9ae0803e85ea708d67dc3bf5655f89d12cb124fc7d7c32d0be6bedec7dffb20355a55ed501dc76321f8ccd3912ade2ef5b4e381c1 + +Source: Scripts/charms/simple/mod/charms.osm/charms/osm/sshproxy.py +Algorithm: SHA-512 +Hash: 5dca5f5c326c3ed2c945d734566370399e470fa42ce51c73ca10a32de8ef97878a68a5434769a5e1637d26c4de624041c1b65a2981e3ac5a44beec0c6dc55138 + +Source: Scripts/charms/simple/mod/charms.osm/charms/osm/libansible.py +Algorithm: SHA-512 +Hash: f3ee2514ad6100f35898b52ccb7b0ba928d8e510c9deff9ad688b5daed4330fb7c9c6d2bf60ffa197da7e7c53af58764cf972e46c37357114464a56bf9637f82 + +Source: Scripts/charms/simple/mod/charms.osm/charms/osm/ns.py +Algorithm: SHA-512 +Hash: 8ceaeb49c67a7389a418580684bb5b7a168d5c2a1b6f0a0f5f0bc9b1346e4b5e56cdd2548036357dd3e5d119cb0af305ac1becaf4e1e8f6abf55718b74b8e628 + +Source: Scripts/charms/simple/mod/operator/.travis.yml +Algorithm: SHA-512 +Hash: 0b3975327a0a7dce376426596e5733fca5f49812d92580ca87286e3065e293c50b98e32569cbb8987b4e7ff6272661c7e1c5e915b0fa448f24ecdd3160b61add + +Source: Scripts/charms/simple/mod/operator/LICENSE.txt +Algorithm: SHA-512 +Hash: 98f6b79b778f7b0a15415bd750c3a8a097d650511cb4ec8115188e115c47053fe700f578895c097051c9bc3dfb6197c2b13a15de203273e1a3218884f86e90e8 + +Source: Scripts/charms/simple/mod/operator/CODE_OF_CONDUCT.md +Algorithm: SHA-512 +Hash: ef7ce87a56dc9bcac75d3ed616a4a8578df7dbc7dcdc2545e6f36fd742055ed7035c45beece89c9b3e1327c0d786a2f6ec02ed3e06794d5cdc9f768b81fde404 + +Source: Scripts/charms/simple/mod/operator/.gitignore +Algorithm: SHA-512 +Hash: d54cfc155cb535db03525477d64395a690107d0b23fde5159b7eb9313a7c603dd31e79978763e519e4fe606103173aaa34af883bb44e9f3e9163e02a982c53b8 + +Source: Scripts/charms/simple/mod/operator/run_tests +Algorithm: SHA-512 +Hash: b2eaf4a81e13c4cbb0ae6ebce5804f37363cfd9cbe3a3d4e47a5756d96b745777f146d9d5a4c08855a1e0f9d793d392cd9831935b71d61a66e8eaecca9f98e84 + +Source: Scripts/charms/simple/mod/operator/.git +Algorithm: SHA-512 +Hash: 9f8e6441a6baed3887162fd8139ff5ef31c63e4dad3eff7dc07b9f238a6b51d7b2a82f95ab0c8dbbbb567075a9479e668f39a81f3fbde6b8bc83898cab825bdd + +Source: Scripts/charms/simple/mod/operator/README.md +Algorithm: SHA-512 +Hash: 5704da74b0fd8d2ba327962bd9335e84c756b9d0c2fbe4f455bcfca74b93cb18e561ccb2b84a97902c210ab613c656183e36c5380f885a68424600cd88521101 + +Source: Scripts/charms/simple/mod/operator/requirements.txt +Algorithm: SHA-512 +Hash: e384a8e87e580c4142f59d9459136354f84c93c0e8cb922ae6e82364770ced27a2a4075446287a5ca861bca50929c74b84ec288c158cff5272f4dcb1c98b17d6 + +Source: Scripts/charms/simple/mod/operator/requirements-dev.txt +Algorithm: SHA-512 +Hash: 1a30b7fed31b1fe2680cbf59c21416a4b7ef9ed4b7dfeda0e9b906180d89e49d555b1cebea844d294b512ead39169da47e75785f8691e552e77cf16184bebdac + +Source: Scripts/charms/simple/mod/operator/build_docs +Algorithm: SHA-512 +Hash: c4b2de6da3596f2a447c6585800cd400873526937a58f734b9c3a9a302a76fc8b3aa3d9e13eec31d688d750cc1db1dc5ad3a4523682828fec15462a77d15215b + +Source: Scripts/charms/simple/mod/operator/.readthedocs.yaml +Algorithm: SHA-512 +Hash: b94452f30e7b9c38cdfe4131d84e00c1bd8b8cfe53e93041759c5dcdda83cc37ce610df6213a45628acfe7aaa8c18a44a8678d090ff1108f6c32880bf6d7ef35 + +Source: Scripts/charms/simple/mod/operator/setup.py +Algorithm: SHA-512 +Hash: 8071437b0fd3b252ebfc35791d3adcd9fd65bd21bd52c9ad3129b923734f02070c3c4eea382d7785e2650c70b464f844ced9d1ab5131cfc27b1b77f8e3d48a84 + +Source: Scripts/charms/simple/mod/operator/.flake8 +Algorithm: SHA-512 +Hash: 30bb1b8436032b8e7887ecc3d7de40432240d57401c2e8bb9787becbf4b97c4714a83a2a368d9be2b40debd0d409d3d1de257f584acf97a0aa8861d6ce03a40c + +Source: Scripts/charms/simple/mod/operator/test/test_log.py +Algorithm: SHA-512 +Hash: 5b87cbb4ec2c0a66f1d92684d58ceeb2fafadfcf7d16b4c92266c6cb4071474badd40ee19b2e659a8544ac76b699fc97338fcbae039104bfeac8b6314584243d + +Source: Scripts/charms/simple/mod/operator/test/__init__.py +Algorithm: SHA-512 +Hash: cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e + +Source: Scripts/charms/simple/mod/operator/test/test_main.py +Algorithm: SHA-512 +Hash: d61833cfc3e7d51e0ce1f4e0c4c784f5c59a7e6a1ac38f69828b99ff47d7506092183c39f2170266c63090438cae467683664978efad2df907dbbd7b767c4b11 + +Source: Scripts/charms/simple/mod/operator/test/test_jujuversion.py +Algorithm: SHA-512 +Hash: 1541cb9dea78636d4d064f7a4d4293f0016331ec0392b9a70bafab11bd3e6fff2746541faaf7ec6844119c721055b8281f8b54da6136c3b9a666ec57a8768e23 + +Source: Scripts/charms/simple/mod/operator/test/test_charm.py +Algorithm: SHA-512 +Hash: be26db4e2225597e986169b4cadac1702c068726fc41c4c9697d16db2b00f8c9b0cb9acbca2898f08cdb9e050cf32f43488e2d81e91a0219b3d8472782077535 + +Source: Scripts/charms/simple/mod/operator/test/test_storage.py +Algorithm: SHA-512 +Hash: 78f32ef6c283d1b334337df1477d0ea428451e7d2f4dca86b1e953b4265311c8fbc08d04892eb014be2e99da545ce411115944a44d32711753eee628324e45e2 + +Source: Scripts/charms/simple/mod/operator/test/test_helpers.py +Algorithm: SHA-512 +Hash: 1489f50c547d452dd791226f13e9aabf0736cf624b8ff9b30f73b71bdd84647e544fc636bbf7e7b7cca5138cd706eddcbc51992943c77068fc2438b1e24de1ac + +Source: Scripts/charms/simple/mod/operator/test/test_framework.py +Algorithm: SHA-512 +Hash: 660ec9f7022e0eb901c6208b10de8bda53f858e4df23773ab0d2f35e4b2ff5e4f45afc75453f4cd8b2f6a9b2e0768ddd1346a29e6338a805e3733665ece26837 + +Source: Scripts/charms/simple/mod/operator/test/test_model.py +Algorithm: SHA-512 +Hash: c4694d11aa9b347c32a3a426f68d790ce7909c00506f75af8ddcdc19b8a498cd73882b47415d69996e1d901328c44411558e206150bca6ebe1aae5fe960a7248 + +Source: Scripts/charms/simple/mod/operator/test/test_lib.py +Algorithm: SHA-512 +Hash: f611b23186eea07ca0eaf696de678ecc062b94cf110117f91ff83fa985616520c707e180baf74d0aedc9d84c2b89c1256ae1f71ea90240d7db559a9e58d0ff79 + +Source: Scripts/charms/simple/mod/operator/test/test_testing.py +Algorithm: SHA-512 +Hash: 1f308b555e2101d4e81f7d4d9d0a2411f4bd450a00e5cd93fdf5fce781aa40a3e3d293a9a0552ed6101ab1fcad755011754b4e395d243b75fe9e4c9baa7b915a + +Source: Scripts/charms/simple/mod/operator/test/test_infra.py +Algorithm: SHA-512 +Hash: b4caa7c6495b8aacc3d49cbb31437e394303c4f7e008fb1c4362f85140e5a088440ae09b99e4a72ce3eea6c647e7303ab8dbff35d84b0b7218c391e60fc327e8 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/config.yaml +Algorithm: SHA-512 +Hash: 39d1a9df3b4dd476d393d49ee81c2200824fe15bac321dc0b99e0210287b343070850f98b3928be448c3a38e232ed21694cc9e209c02dc983c4aba2611240765 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/actions.yaml +Algorithm: SHA-512 +Hash: 5b561ddc315a4c5f3bfec125beb2b8a447d1d23bc558ba190eb11d372c3e446ad812da2f28732124a14268f2c28af1af34d0ad3d90e082aec2c0fe77d79fb952 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/metadata.yaml +Algorithm: SHA-512 +Hash: b240e0e5d70007397c37c8f15a7dc01a907ef1a67edee15b453e0239373c700ccff44c5b89e6b0d563321ba0988f706e89c26c1f1eb390fdbb7b4674e77049f3 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/src/charm.py +Algorithm: SHA-512 +Hash: eb944cbf9156fea4c9920a5d28a1b57a14e0b53555dedfa251a21c8bce16cb4a8580721f52ecd6d5ec3b791cef81359a77dd366fd0c5de25c59053357bb84f74 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/__init__.py +Algorithm: SHA-512 +Hash: cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/charm.py +Algorithm: SHA-512 +Hash: 72bab681b72a29ab7649acb3e6abb9d117c103bb944ebbbdbef1a4b58c997670a305051c05f98fa617f62188d17356b81fcef4a97e0ce9ed6247daf6ac586241 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/model.py +Algorithm: SHA-512 +Hash: 2d4098235b1c12a1de8b256fe6ed14d4b21e5bfd0464c56e4c430be20e1c85dd70a624906693a5cbb1ba5e8ba1dce5cea7905d3150166976fee1e9fe780e8f50 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/main.py +Algorithm: SHA-512 +Hash: 20eae0c37fab197efcc59a97ed62b378fff0776193910477887b220a13ef11ba98fc059e247390e503f17cd2bcf71e106f6296e5bc02ecd274295ade14da905f + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/__init__.py +Algorithm: SHA-512 +Hash: 6756076d4b3ab0a9f47c5fd6b06088037749dc0d96075186cb8dc0770fbf2b528a3e69d9364c7e3843efbaafc5ff4c156dbb198a3649692fb2e9134101e6fde7 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/log.py +Algorithm: SHA-512 +Hash: b078a1df98276c1bd52ae7ef035912611f176dce4902d6f73aaae1d0527193673ddfbb03a7cad81d98dbb3ca29a7acac317d68a2b663483d20494bb5d9a0641b + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/framework.py +Algorithm: SHA-512 +Hash: ea92ee16fa046312ac4b684d3bc2e6e7be8600ac67854ce5837284299ef0e5787a2d9770942a3ddf76141e767aab2cc8c4da311ff2c6df69b59fd9e087139240 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/jujuversion.py +Algorithm: SHA-512 +Hash: 03d0306f1650e522012c96b7f9ed6770ca17e8fb6a3c7a715c6dfbb9c1a86caae640419dfde053af6675dfad6463ed2a97c8d32215328df4906f496aeba56c46 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/testing.py +Algorithm: SHA-512 +Hash: 9b5adebe447c8f2b93a5cd2b6fc65a9582c07260fb8390924ba74bc6b90d296f7eca4caa899dcf62356821dd9321287f99dfe8da9673296c5ed483ce61284769 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/storage.py +Algorithm: SHA-512 +Hash: a9e6376d721cca8e8239ce54a8b42cfd48ae91a21f4ff770d77f349894f7bb1bc737221923b3e15e893b95aa4f88166d8eefd57724cbb361c42ca567939a7496 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/version.py +Algorithm: SHA-512 +Hash: 340e7e1106caaf0ab155beb1e3daf16a775bc089b137d39adca3450a831e1adc6432b8dc65a3e15ab1d45ba8a8ceabb2cb3d7b16b1705fb68d77a1336aaabd91 + +Source: Scripts/charms/simple/mod/operator/test/charms/test_main/lib/ops/lib/__init__.py +Algorithm: SHA-512 +Hash: 5dd21c89546ce53b877d3a75d2148d877b95f5116beb6f6031e509fa3a843ea9a1df85e407abc6651dbb173c0a67a1f84342aae1c2e49ea433f8385325f01988 + +Source: Scripts/charms/simple/mod/operator/test/bin/relation-list +Algorithm: SHA-512 +Hash: af68980de3c0315899e8e860f59c640f4551a255d310a6a4a25111e4c5f7d11006067954270c1938ebdfca1f641c472b1938ed65630ac7ccf0bb76547e82815c + +Source: Scripts/charms/simple/mod/operator/test/bin/relation-ids +Algorithm: SHA-512 +Hash: 01b7a5b4334df5b3386879365eb0a99c975c81692aaa595b9be5004824668f74c49cc0101c5c1df38f3ca5bc2cc3bcde3e769d38d47a06c16fcba0c0a369917e + +Source: Scripts/charms/simple/mod/operator/docs/index.rst +Algorithm: SHA-512 +Hash: 96d7a61f54d2501eecf089b5507184a37f2697a2bbd469a3b2b2473bef04ea456b94eebf89fc4b74ffb14bc92bb28b1b65ca13688f0d8a736a7d1132ba017878 + +Source: Scripts/charms/simple/mod/operator/docs/conf.py +Algorithm: SHA-512 +Hash: 97d5fd3b5ae607e2ebb1d485c2a4d1710e06b8fccc43b8e7605dd5db7ace947a338d16afb067ec5445ce03c93295ac74de0c1a00db5b27a42c13857ab15d50a3 + +Source: Scripts/charms/simple/mod/operator/docs/requirements.txt +Algorithm: SHA-512 +Hash: eb59fb95c69dc6c8019253dfb703f8f61195837d08f0091dd271b3c116e92b3e670a411ec6bc87eb6e73bf30662329fa00dc47bae489327fffeb1d037e79d959 + +Source: Scripts/charms/simple/mod/operator/ops/charm.py +Algorithm: SHA-512 +Hash: 72bab681b72a29ab7649acb3e6abb9d117c103bb944ebbbdbef1a4b58c997670a305051c05f98fa617f62188d17356b81fcef4a97e0ce9ed6247daf6ac586241 + +Source: Scripts/charms/simple/mod/operator/ops/model.py +Algorithm: SHA-512 +Hash: 2d4098235b1c12a1de8b256fe6ed14d4b21e5bfd0464c56e4c430be20e1c85dd70a624906693a5cbb1ba5e8ba1dce5cea7905d3150166976fee1e9fe780e8f50 + +Source: Scripts/charms/simple/mod/operator/ops/main.py +Algorithm: SHA-512 +Hash: 20eae0c37fab197efcc59a97ed62b378fff0776193910477887b220a13ef11ba98fc059e247390e503f17cd2bcf71e106f6296e5bc02ecd274295ade14da905f + +Source: Scripts/charms/simple/mod/operator/ops/__init__.py +Algorithm: SHA-512 +Hash: 6756076d4b3ab0a9f47c5fd6b06088037749dc0d96075186cb8dc0770fbf2b528a3e69d9364c7e3843efbaafc5ff4c156dbb198a3649692fb2e9134101e6fde7 + +Source: Scripts/charms/simple/mod/operator/ops/log.py +Algorithm: SHA-512 +Hash: b078a1df98276c1bd52ae7ef035912611f176dce4902d6f73aaae1d0527193673ddfbb03a7cad81d98dbb3ca29a7acac317d68a2b663483d20494bb5d9a0641b + +Source: Scripts/charms/simple/mod/operator/ops/framework.py +Algorithm: SHA-512 +Hash: ea92ee16fa046312ac4b684d3bc2e6e7be8600ac67854ce5837284299ef0e5787a2d9770942a3ddf76141e767aab2cc8c4da311ff2c6df69b59fd9e087139240 + +Source: Scripts/charms/simple/mod/operator/ops/jujuversion.py +Algorithm: SHA-512 +Hash: 03d0306f1650e522012c96b7f9ed6770ca17e8fb6a3c7a715c6dfbb9c1a86caae640419dfde053af6675dfad6463ed2a97c8d32215328df4906f496aeba56c46 + +Source: Scripts/charms/simple/mod/operator/ops/testing.py +Algorithm: SHA-512 +Hash: 9b5adebe447c8f2b93a5cd2b6fc65a9582c07260fb8390924ba74bc6b90d296f7eca4caa899dcf62356821dd9321287f99dfe8da9673296c5ed483ce61284769 + +Source: Scripts/charms/simple/mod/operator/ops/storage.py +Algorithm: SHA-512 +Hash: a9e6376d721cca8e8239ce54a8b42cfd48ae91a21f4ff770d77f349894f7bb1bc737221923b3e15e893b95aa4f88166d8eefd57724cbb361c42ca567939a7496 + +Source: Scripts/charms/simple/mod/operator/ops/version.py +Algorithm: SHA-512 +Hash: 340e7e1106caaf0ab155beb1e3daf16a775bc089b137d39adca3450a831e1adc6432b8dc65a3e15ab1d45ba8a8ceabb2cb3d7b16b1705fb68d77a1336aaabd91 + +Source: Scripts/charms/simple/mod/operator/ops/lib/__init__.py +Algorithm: SHA-512 +Hash: 5dd21c89546ce53b877d3a75d2148d877b95f5116beb6f6031e509fa3a843ea9a1df85e407abc6651dbb173c0a67a1f84342aae1c2e49ea433f8385325f01988 + +Source: k8s_proxy_charm_vnfd.yaml +Algorithm: SHA-512 +Hash: f7421654c21292146a7be04b38e2a931339795e167cbb5a64f1ab67a7b29b67902d8222b3c81dcb02a510d5577d5f6094e9d34901d4c2722e57b6048e4ec871d + diff --git a/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.yaml b/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.yaml new file mode 100644 index 00000000..01fd67db --- /dev/null +++ b/charm-packages/SOL004_k8s_proxy_charm_vnf/k8s_proxy_charm_vnfd.yaml @@ -0,0 +1,109 @@ +vnfd: + description: A VNF consisting of 1 VDU connected to two external VL, and one for + data and another one for management + df: + - id: default-df + instantiation-level: + - id: default-instantiation-level + vdu-level: + - number-of-instances: 1 + vdu-id: mgmtVM + vdu-profile: + - id: mgmtVM + min-number-of-instances: 1 + lcm-operations-configuration: + operate-vnf-op-config: + day1-2: + - config-primitive: + - name: touch + execution-environment-ref: simple-ee + parameter: + - data-type: STRING + default-value: /home/ubuntu/touched + name: filename + config-access: + ssh-access: + default-user: ubuntu + required: true + execution-environment-list: + - id: simple-ee + juju: + charm: simple + cloud: k8s + id: k8s_proxy_charm-vnf + initial-config-primitive: + - name: config + execution-environment-ref: simple-ee + parameter: + - name: ssh-hostname + value: + - name: ssh-username + value: ubuntu + - name: ssh-password + value: osm4u + seq: 1 + - name: touch + execution-environment-ref: simple-ee + parameter: + - data-type: STRING + name: filename + value: /home/ubuntu/first-touch + seq: 2 + ext-cpd: + - id: vnf-mgmt-ext + int-cpd: + cpd: mgmtVM-eth0-int + vdu-id: mgmtVM + - id: vnf-data-ext + int-cpd: + cpd: dataVM-xe0-int + vdu-id: mgmtVM + id: k8s_proxy_charm-vnf + mgmt-cp: vnf-mgmt-ext + product-name: k8s_proxy_charm-vnf + sw-image-desc: + - id: ubuntu18.04 + image: ubuntu18.04 + name: ubuntu18.04 + - id: ubuntu18.04-azure + name: ubuntu18.04-azure + image: Canonical:UbuntuServer:18.04-LTS:latest + vim-type: azure + - id: ubuntu18.04-gcp + name: ubuntu18.04-gcp + image: ubuntu-os-cloud:image-family:ubuntu-1804-lts + vim-type: gcp + vdu: + - cloud-init-file: cloud-config.txt + id: mgmtVM + int-cpd: + - id: mgmtVM-eth0-int + virtual-network-interface-requirement: + - name: mgmtVM-eth0 + position: 1 + virtual-interface: + type: PARAVIRT + - id: dataVM-xe0-int + virtual-network-interface-requirement: + - name: dataVM-xe0 + position: 2 + virtual-interface: + type: PARAVIRT + name: mgmtVM + sw-image-desc: ubuntu18.04 + alternative-sw-image-desc: + - ubuntu18.04-azure + - ubuntu18.04-gcp + virtual-compute-desc: mgmtVM-compute + virtual-storage-desc: + - mgmtVM-storage + version: 1.0 + virtual-compute-desc: + - id: mgmtVM-compute + virtual-cpu: + num-virtual-cpu: 1 + virtual-memory: + size: 1.0 + virtual-storage-desc: + - id: mgmtVM-storage + size-of-storage: 10 diff --git a/charm-packages/SOL007_k8s_proxy_charm_ns/ChangeLog.txt b/charm-packages/SOL007_k8s_proxy_charm_ns/ChangeLog.txt new file mode 100644 index 00000000..8f45952f --- /dev/null +++ b/charm-packages/SOL007_k8s_proxy_charm_ns/ChangeLog.txt @@ -0,0 +1,5 @@ + +1.0.0 + +- Package converted with OSM package migration tool. + diff --git a/charm-packages/SOL007_k8s_proxy_charm_ns/Files/icons/osm.png b/charm-packages/SOL007_k8s_proxy_charm_ns/Files/icons/osm.png new file mode 100644 index 0000000000000000000000000000000000000000..62012d2a2b491bdcd536d62c3c3c863c0d8c1b33 GIT binary patch literal 55888 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 QVa%F-+q93SJpA+j4_IIKX8-^I literal 0 HcmV?d00001 diff --git a/charm-packages/SOL007_k8s_proxy_charm_ns/Licenses/license.lic b/charm-packages/SOL007_k8s_proxy_charm_ns/Licenses/license.lic new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/charm-packages/SOL007_k8s_proxy_charm_ns/Licenses/license.lic @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.mf b/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.mf new file mode 100644 index 00000000..a5f222fb --- /dev/null +++ b/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.mf @@ -0,0 +1,12 @@ + +nsd_invariant_id: default-id +nsd_name: default-name +nsd_designer: OSM +nsd_file_structure_version: 1.0 +nsd_release_date_time: 2021-11-09T17:51:14.904155-03:00 +compatible_specification_versions: 3.3.1 + +Source: k8s_proxy_charm_nsd.yaml +Algorithm: SHA-512 +Hash: 47baf150e903562c0790ed4594ace33afffd84efc99b28aae3f9672805b58eb0b083546360f0e458da83d2e593a895448a391b59af1e3c8c8a913f4e57bb87f3 + diff --git a/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.yaml b/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.yaml new file mode 100644 index 00000000..beae3243 --- /dev/null +++ b/charm-packages/SOL007_k8s_proxy_charm_ns/k8s_proxy_charm_nsd.yaml @@ -0,0 +1,37 @@ +nsd: + nsd: + - description: NS with 2 VNFs with cloudinit connected by datanet and mgmtnet VLs + df: + - id: default-df + vnf-profile: + - id: '1' + virtual-link-connectivity: + - constituent-cpd-id: + - constituent-base-element-id: '1' + constituent-cpd-id: vnf-mgmt-ext + virtual-link-profile-id: mgmtnet + - constituent-cpd-id: + - constituent-base-element-id: '1' + constituent-cpd-id: vnf-data-ext + virtual-link-profile-id: datanet + vnfd-id: k8s_proxy_charm-vnf + - id: '2' + virtual-link-connectivity: + - constituent-cpd-id: + - constituent-base-element-id: '2' + constituent-cpd-id: vnf-mgmt-ext + virtual-link-profile-id: mgmtnet + - constituent-cpd-id: + - constituent-base-element-id: '2' + constituent-cpd-id: vnf-data-ext + virtual-link-profile-id: datanet + vnfd-id: k8s_proxy_charm-vnf + id: k8s_proxy_charm-ns + name: k8s_proxy_charm-ns + version: '1.0' + virtual-link-desc: + - id: mgmtnet + mgmt-network: 'true' + - id: datanet + vnfd-id: + - k8s_proxy_charm-vnf -- GitLab