diff --git a/.gitmodules b/.gitmodules
index 3613fb5ed360a86ba0e157e513bfa121ce1555a5..ce1de377d7427a2fa781036b685954701e73065c 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 0000000000000000000000000000000000000000..8f45952f8bafe6be891e7fa45137533fd72b66d7
--- /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
Binary files /dev/null and b/SOL004_hackfest_basic_vnf/Files/icons/osm.png differ
diff --git a/SOL004_hackfest_basic_vnf/Licenses/license.lic b/SOL004_hackfest_basic_vnf/Licenses/license.lic
new file mode 100644
index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /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 0000000000000000000000000000000000000000..e3bca28a016e5769b5fcb4347090b858fd65dd51
--- /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 0000000000000000000000000000000000000000..4625e424a1ac97679e4053284cd719c02fbef927
--- /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 0000000000000000000000000000000000000000..8f45952f8bafe6be891e7fa45137533fd72b66d7
--- /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
Binary files /dev/null and b/SOL007_hackfest_basic_ns/Files/icons/osm.png differ
diff --git a/SOL007_hackfest_basic_ns/Licenses/license.lic b/SOL007_hackfest_basic_ns/Licenses/license.lic
new file mode 100644
index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /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 0000000000000000000000000000000000000000..9fb7bd74252e89c0f19129421826c55877680e59
--- /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 0000000000000000000000000000000000000000..be76b264172ba0f906d2b6d84e48c7542154e9e4
--- /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 0000000000000000000000000000000000000000..8f45952f8bafe6be891e7fa45137533fd72b66d7
--- /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
Binary files /dev/null and b/charm-packages/SOL004_k8s_proxy_charm_vnf/Files/icons/osm.png differ
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 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /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 0000000000000000000000000000000000000000..f9882c47bba960254e0381e5b5ab1c0688de6cf5
--- /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 0000000000000000000000000000000000000000..93e3cab01c0843418c57e4bc918eb933f2daf934
--- /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 0000000000000000000000000000000000000000..e23b12b7bcfddfd49d4efbc1b0ac72d92579778a
--- /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 0000000000000000000000000000000000000000..e23b12b7bcfddfd49d4efbc1b0ac72d92579778a
--- /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 0000000000000000000000000000000000000000..e23b12b7bcfddfd49d4efbc1b0ac72d92579778a
--- /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 0000000000000000000000000000000000000000..32fd26ae7d63d42edef33982d5438669b191a361
--- /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 0000000000000000000000000000000000000000..25be4056282e48ce946632025be8b466557e3171
--- /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 0000000000000000000000000000000000000000..f323a3af88f3011ef9fde794cdfe343be8665abb
--- /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 0000000000000000000000000000000000000000..e2c311e5be8515a7fe19c05dfed9f042ded0576b
--- /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 0000000000000000000000000000000000000000..f17b2969db298b21bc47bbe1d3614ccff93e9c6e
--- /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 0000000000000000000000000000000000000000..d898de859fc444814bc19a7f8f0caaaec6f7e5f4
--- /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', {})  # {<parameter name>: <JSON Schema definition>}
+        self.required = raw.get('required', [])  # [<parameter name>, ...]
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 0000000000000000000000000000000000000000..b7c4749ff2b5bfb4f354bf1a8d4cd6ed64cf0da5
--- /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 '<BoundEvent {} bound to {}.{} at {}>'.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 0000000000000000000000000000000000000000..b2b8177dbe396f0d8c46b86e26af6b4e54ea046d
--- /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<major>\d{1,9})\.(?P<minor>\d{1,9})       # <major> and <minor> numbers are always there
+    ((?:\.|-(?P<tag>[a-z]+))(?P<patch>\d{1,9}))? # sometimes with .<patch> or -<tag><patch>
+    (\.(?P<build>\d{1,9}))?$                     # and sometimes with a <build> 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 0000000000000000000000000000000000000000..edb9fcacea6f0173aed9f07ca8a683cfead989cc
--- /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 0000000000000000000000000000000000000000..4aac5543aec4d84dc393e79b772a30284712d6d4
--- /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 0000000000000000000000000000000000000000..6dc31c3575044796e8fe1f61b8415395689d6339
--- /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 0000000000000000000000000000000000000000..b96e89154ea9cec2b62a4fab4649412e115c304e
--- /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-name>-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 0000000000000000000000000000000000000000..d4310ce1cfbb707c6278b70f84a9751da3ce07af
--- /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 0000000000000000000000000000000000000000..b4b3fe071216238007c9f3847ca9556be626bf6b
--- /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 0000000000000000000000000000000000000000..15e5478555ee0fa948bfb0ad57cc79ba7cef3721
--- /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 <tag>-<#commits>-g<hex>[-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 <tag>+<#commits>.g<hex>[.dirty]
+                # which is PEP440-compliant (as long as <tag> 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 0000000000000000000000000000000000000000..4b5b352850eb98a4e2028942957cebc622a2395e
--- /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 0000000000000000000000000000000000000000..a7c5b6a8af22d715276125afc3ed26f0436c71af
--- /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 0000000000000000000000000000000000000000..824aa2d8996ea548c913317c2df6bac258f0737b
--- /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 0000000000000000000000000000000000000000..e23b12b7bcfddfd49d4efbc1b0ac72d92579778a
--- /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 0000000000000000000000000000000000000000..36c8d1bf2cdebbc4e50d1e8348003f64f419cd0b
--- /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 0000000000000000000000000000000000000000..1a151692568e19871998138d02bc8acce2d8fa7f
--- /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 0000000000000000000000000000000000000000..01fd67dbe51eeb68c9747754462407e7b0798263
--- /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: <rw_mgmt_ip>
+            - 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 0000000000000000000000000000000000000000..8f45952f8bafe6be891e7fa45137533fd72b66d7
--- /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
Binary files /dev/null and b/charm-packages/SOL007_k8s_proxy_charm_ns/Files/icons/osm.png differ
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 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /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 0000000000000000000000000000000000000000..a5f222fb9341dfe11838813e21afea3d9997f20d
--- /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 0000000000000000000000000000000000000000..beae3243b71a197f06f18c76c3fb3d277842df19
--- /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