Initial refactoring of charm

This commit is contained in:
James Page 2020-03-11 11:21:46 +00:00
commit fe6d428d56
27 changed files with 2527 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
build/
.local/
.testrepository/
.tox/
func-results.json
test-charm/
**/__pycache__
.stestr

3
.stestr.conf Normal file
View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=./unit_tests
top_dir=./

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# Overview
TrilioVault Horizon Plugin is a plugin of TrilioVault which is installed
on the Openstack and provides TrilioVault UI components.
# Usage
TrilioVault Horizon Plugin is a sub-ordinate charm of openstack-dashboard
and relies on services from openstack-dashboard.
Steps to deploy the charm:
juju deploy trilio-horizon-plugin
juju deploy openstack-dashboard
juju add-relation trilio-horizon-plugin openstack-dashboard
# Configuration
python-version: "Openstack base python version(2 or 3)"
NOTE - Default value is set to "3". Please ensure to update this based on python version since installing
python3 packages on python2 based setup might have unexpected impact.
TrilioVault Packages are downloaded from the repository added in below config parameter. Please change this only if you wish to download
TrilioVault Packages from a different source.
triliovault-pkg-source: Repository address of triliovault packages
# Contact Information
Trilio Support <support@trilio.com>

16
copyright Normal file
View File

@ -0,0 +1,16 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
Files: *
Copyright: 2018, Trilio
License: Apache-2.0
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.

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
# Requirements to build the charm
charm-tools
simplejson
flake8

33
src/README.md Normal file
View File

@ -0,0 +1,33 @@
# Overview
TrilioVault Horizon Plugin is a plugin of TrilioVault which is installed
on the Openstack and provides TrilioVault UI components.
# Usage
TrilioVault Horizon Plugin is a sub-ordinate charm of openstack-dashboard
and relies on services from openstack-dashboard.
Steps to deploy the charm:
juju deploy trilio-horizon-plugin
juju deploy openstack-dashboard
juju add-relation trilio-horizon-plugin openstack-dashboard
# Configuration
python-version: "Openstack base python version(2 or 3)"
NOTE - Default value is set to "3". Please ensure to update this based on python version since installing
python3 packages on python2 based setup might have unexpected impact.
TrilioVault Packages are downloaded from the repository added in below config parameter. Please change this only if you wish to download
TrilioVault Packages from a different source.
triliovault-pkg-source: Repository address of triliovault packages
# Contact Information
Trilio Support <support@trilio.com>

9
src/config.yaml Normal file
View File

@ -0,0 +1,9 @@
options:
python-version:
type: int
default: 3
description: Openstack base python version(2 or 3)
triliovault-pkg-source:
type: string
default: "deb [trusted=yes] https://apt.fury.io/triliodata-3-4/ /"
description: Repository address of triliovault packages

16
src/copyright Normal file
View File

@ -0,0 +1,16 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
Files: *
Copyright: 2018, Trilio
License: Apache-2.0
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.

View File

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = "workloads_admin"
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = "admin"
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = "backups-admin"
# Python panel class of the PANEL to be added.
ADD_PANEL = "dashboards.workloads_admin.panel.Workloads_admin"
ADD_INSTALLED_APPS = ["dashboards"]
DISABLED = False

View File

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = "backups-admin"
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _("Backups-Admin")
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = "admin"

View File

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = "workloads"
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = "project"
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = "backups"
# Python panel class of the PANEL to be added.
ADD_PANEL = "dashboards.workloads.panel.Workloads"
DISABLED = False

View File

@ -0,0 +1,8 @@
from django.utils.translation import ugettext_lazy as _
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = "backups"
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _("Backups")
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = "project"

View File

@ -0,0 +1,9 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = "settings"
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = "project"
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = "backups"
# Python panel class of the PANEL to be added.
ADD_PANEL = "dashboards.settings.panel.Settings"
DISABLED = False

View File

@ -0,0 +1,17 @@
import sys
import settings
import subprocess
ls = settings.INSTALLED_APPS
data = ""
for app in ls:
if app != "dashboards":
data += "-i " + str(app) + " "
cmd = (
"{} /usr/share/openstack-dashboard/manage.py collectstatic"
" --noinput {}".format(sys.executable, data)
)
subprocess.call(cmd, shell=True)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,124 @@
from django import template
from openstack_dashboard import api
from openstack_dashboard import policy
from datetime import datetime
from django.template.defaultfilters import stringfilter
import pytz
register = template.Library()
@register.filter(name="getusername")
def get_user_name(user_id, request):
user_name = user_id
if policy.check((("identity", "identity:get_user"),), request):
try:
user = api.keystone.user_get(request, user_id)
if user:
user_name = user.username
except Exception:
pass
return user_name
@register.filter(name="getprojectname")
def get_project_name(project_id, request):
project_name = project_id
try:
project_info = api.keystone.tenant_get(request, project_id, admin=True)
if project_info:
project_name = project_info.name
except Exception:
pass
return project_name
def get_time_zone(request):
tz = "UTC"
try:
tz = request._get_cookies()["django_timezone"]
except Exception:
try:
tz = request.COOKIES["django_timezone"]
except Exception:
pass
return tz
def get_local_time(record_time, input_format, output_format, tz):
"""
Convert and return the date and time - from GMT to local time
"""
try:
if not record_time or record_time is None or record_time == "":
return ""
else:
if not input_format or input_format is None or input_format == "":
input_format = "%Y-%m-%dT%H:%M:%S.%f"
if (
not output_format or
output_format is None or
output_format == ""
):
output_format = "%m/%d/%Y %I:%M:%S %p"
local_time = datetime.strptime(record_time, input_format)
local_tz = pytz.timezone(tz)
from_zone = pytz.timezone("UTC")
local_time = local_time.replace(tzinfo=from_zone)
local_time = local_time.astimezone(local_tz)
local_time = datetime.strftime(local_time, output_format)
return local_time
except Exception:
pass
return record_time
@register.filter(name="gettime")
def get_time_for_audit(time_stamp, request):
display_time = time_stamp
try:
time_zone_of_ui = get_time_zone(request)
display_time = get_local_time(
time_stamp,
"%I:%M:%S.%f %p - %m/%d/%Y",
"%I:%M:%S %p - %m/%d/%Y",
time_zone_of_ui,
)
except Exception:
pass
return display_time
@register.filter(name="getsnapshotquantifier")
def display_time_quantifier(seconds):
intervals = (
("weeks", 604800), # 60 * 60 * 24 * 7
("days", 86400), # 60 * 60 * 24
("hours", 3600), # 60 * 60
("minutes", 60),
("seconds", 1),
)
result = []
granularity = 4
for name, count in intervals:
value = seconds // count
if value:
seconds -= value * count
if value == 1:
name = name.rstrip("s")
result.append("{} {}".format(value, name))
else:
# Add a blank if we're in the middle of other values
if len(result) > 0:
result.append(None)
return ", ".join([x for x in result[:granularity] if x is not None])
@register.filter(name="custom_split")
@stringfilter
def custom_split(value, key):
key = int(key)
return value.split("_")[key]

23
src/icon.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:#77BC1F;}
.st1{fill:#FFFFFF;}
</style>
<circle class="st0" cx="500" cy="500.9" r="492.5"/>
<g>
<path class="st1" d="M500.1,799.9c79.9,0,153.9-30.2,208.3-85c55-55.4,85-131.1,84.5-213.1c0-167.8-128.6-299.2-292.7-299.2h-33.6
v23.1h33.6c151.2,0,269.5,121.3,269.5,276.2c0.6,75.8-27,145.7-77.7,196.8c-50.1,50.3-118.2,78-191.9,78
c-153.7,0-269.5-118.3-269.5-275.1c0-68.6,23.7-133.8,66.9-183.7l-16.6-16.5c-47.4,54.4-73.5,125.4-73.5,200.3
C207.4,671.7,333.2,799.9,500.1,799.9"/>
<path class="st1" d="M500.1,846c92.2,0,177.5-34.8,240.4-98.1c63.4-63.9,98-151.3,97.3-246c0-192.8-147.7-344.4-336.1-345.4h-12.5
V48.3L332.2,215l157.1,166.7v-110h10.8c125.8,0,224.4,101,224.4,230c0.4,63.2-22.5,121.5-64.7,163.9c-41.6,41.9-98.4,65-159.7,65
c-128,0-224.4-98.4-224.4-229c0-56.2,19.2-109.8,54.2-151.3L313.3,334c-39.3,45.8-60.8,105.3-60.8,167.7
c0,143.7,106.4,252.1,247.6,252.1c67.6,0,130.2-25.5,176.1-71.9c46.5-46.9,71.9-110.8,71.4-180.2c0-141.9-108.7-253-247.5-253
h-33.9v74.9L364,215l102.2-108.5v73.1h33.9c176.5,0,314.7,141.6,314.7,322.4c0.7,88.6-31.6,170.1-90.7,229.7
c-58.4,58.8-137.9,91.2-224,91.2c-179.4,0-314.7-138-314.7-321c0-80.9,28.2-157.6,79.5-216.3l-16.5-16.5
c-55.6,63-86.2,145.6-86.2,232.8C162.2,698.1,307.5,846,500.1,846"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

7
src/layer.yaml Normal file
View File

@ -0,0 +1,7 @@
includes:
- 'layer:openstack'
- 'interface:dashboard-plugin'
options:
basic:
use_venv: True
include_system_packages: True

View File

@ -0,0 +1,97 @@
# 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 glob
import os
import shutil
import subprocess
import charmhelpers.core.hookenv as hookenv
import charmhelpers.fetch as fetch
import charms_openstack.charm
HORIZON_PATH = "/usr/share/openstack-dashboard"
MANAGE_PY = os.path.join(HORIZON_PATH, "manage.py")
class TrilioHorizonPluginCharm(charms_openstack.charm.OpenStackCharm):
service_name = name = "trilio-horizon-plugin"
# First release supported
release = "stein"
required_relations = []
package_codenames = {
"tvault-horizon-plugin": collections.OrderedDict([("3", "stein")]),
"python3-horizon-plugin": collections.OrderedDict([("3", "stein")]),
}
def configure_source(self):
with open(
"/etc/apt/sources.list.d/" "trilio-gemfury-sources.list", "w"
) as tsources:
tsources.write(hookenv.config("triliovault-pkg-source"))
fetch.apt_update(fatal=True)
@property
def packages(self):
if hookenv.config("python-version") == 2:
return ["python-workloadmgrclient", "tvault-horizon-plugin"]
return ["python3-workloadmgrclient", "python3-tvault-horizon-plugin"]
# TODO: drop once packaging is updated
def install(self):
self.configure_source()
super().install()
self.copy_files()
self.collectstatic_and_compress()
# TODO: drop once packaging is updated
def upgrade_charm(self):
super().upgrade_charm()
self.copy_files()
self.collectstatic_and_compress()
# TODO: drop when package does this
def copy_files(self):
for panel in glob.glob("src/files/trilio/panels/*"):
shutil.copy(
panel,
os.path.join(
HORIZON_PATH, "openstack_dashboard/local/enabled"
),
)
for templatetag in glob.glob("src/files/trilio/templatetags/*"):
shutil.copy(
templatetag,
os.path.join(HORIZON_PATH, "openstack_dashboard/templatetags"),
)
for index_html in glob.glob(
"/usr/lib/python*/**/workloads_admin/index.html", recursive=True
):
shutil.copy(
"src/files/trilio/templates/workload_admin/index.html",
index_html,
)
# TODO: drop when package does this
def collectstatic_and_compress(self):
python = "/usr/bin/python{}".format(hookenv.config("python-version"))
subprocess.check_call([python, MANAGE_PY, "collectstatic"])
subprocess.check_call([python, MANAGE_PY, "compress", "--force"])

19
src/metadata.yaml Normal file
View File

@ -0,0 +1,19 @@
---
name: trilio-horizon-plugin
summary: TrilioVault Horizon Plugin
maintainer: Trilio Support <support@trilio.io>
description: |
Openstack Dashboard - Horizon plugin for TrilioVault
tags:
- openstack
- storage
- backup
- TVMv3.4
subordinate: true
series:
- xenial
- bionic
requires:
dashboard-plugin:
interface: dashboard-plugin
scope: container

View File

@ -0,0 +1,19 @@
# 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 charms_openstack.charm as charm
# This charm's library contains all of the handler code associated with
# trilio_horizon_plugin
import charm.openstack.trilio_horizon_plugin as trilio_horizon_plugin # noqa
charm.use_defaults("charm.installed", "config.changed", "update-status")

View File

@ -0,0 +1,7 @@
# Unit test requirements
flake8>=2.2.4,<=2.4.1
os-testr>=0.4.1
charms.reactive
mock>=1.2
coverage>=3.6
git+https://github.com/openstack/charms.openstack#egg=charms.openstack

21
src/tox.ini Normal file
View File

@ -0,0 +1,21 @@
# tox (https://tox.readthedocs.io/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
skipsdist = True
envlist = pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
TERM=linux
INTERFACE_PATH={toxinidir}/interfaces
LAYER_PATH={toxinidir}/layers
JUJU_REPOSITORY={toxinidir}/build
[testenv:pep8]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = flake8 {posargs} reactive

7
test-requirements.txt Normal file
View File

@ -0,0 +1,7 @@
# Unit test requirements
flake8>=2.2.4
os-testr>=0.4.1
charms.reactive
mock>=1.2
coverage>=3.6
git+https://github.com/openstack/charms.openstack#egg=charms.openstack

92
tox.ini Normal file
View File

@ -0,0 +1,92 @@
# Source charm: ./tox.ini
# This file is managed centrally by release-tools and should not be modified
# within individual charm repos. See the 'global' dir contents for available
# choices of tox.ini for OpenStack Charms:
# https://github.com/openstack-charmers/release-tools
[tox]
skipsdist = True
envlist = pep8,py3
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
sitepackages = False
# NOTE: Avoid false positives by not skipping missing interpreters.
skip_missing_interpreters = False
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
TERM=linux
LAYER_PATH={toxinidir}/layers
INTERFACE_PATH={toxinidir}/interfaces
JUJU_REPOSITORY={toxinidir}/build
passenv = http_proxy https_proxy INTERFACE_PATH LAYER_PATH JUJU_REPOSITORY
install_command =
pip install {opts} {packages}
deps =
-r{toxinidir}/requirements.txt
[testenv:build]
basepython = python3
commands =
charm-build --log-level DEBUG -o {toxinidir}/build src {posargs}
[testenv:py3]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py35]
basepython = python3.5
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py36]
basepython = python3.6
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py37]
basepython = python3.7
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:pep8]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = flake8 {posargs} src unit_tests
[testenv:cover]
# Technique based heavily upon
# https://github.com/openstack/nova/blob/master/tox.ini
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
setenv =
{[testenv]setenv}
PYTHON=coverage run
commands =
coverage erase
stestr run --slowest {posargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
[coverage:run]
branch = True
concurrency = multiprocessing
parallel = True
source =
.
omit =
.tox/*
*/charmhelpers/*
unit_tests/*
[testenv:venv]
basepython = python3
commands = {posargs}
[flake8]
# E402 ignore necessary for path append before sys module import in actions
ignore = E402,W504

9
unit_tests/__init__.py Normal file
View File

@ -0,0 +1,9 @@
import sys
sys.path.append("src")
sys.path.append("src/lib")
# Mock out charmhelpers so that we can test without it.
import charms_openstack.test_mocks # noqa
charms_openstack.test_mocks.mock_charmhelpers()

View File

@ -0,0 +1,120 @@
# 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 mock
import reactive.trilio_horizon_plugin_handlers as handlers
import charms_openstack.test_utils as test_utils
_when_args = {}
_when_not_args = {}
def mock_hook_factory(d):
def mock_hook(*args, **kwargs):
def inner(f):
# remember what we were passed. Note that we can't actually
# determine the class we're attached to, as the decorator only gets
# the function.
try:
d[f.__name__].append(dict(args=args, kwargs=kwargs))
except KeyError:
d[f.__name__] = [dict(args=args, kwargs=kwargs)]
return f
return inner
return mock_hook
class TestDmapiHandlers(test_utils.PatchHelper):
@classmethod
def setUpClass(cls):
cls._patched_when = mock.patch(
"charms.reactive.when", mock_hook_factory(_when_args)
)
cls._patched_when_started = cls._patched_when.start()
cls._patched_when_not = mock.patch(
"charms.reactive.when_not", mock_hook_factory(_when_not_args)
)
cls._patched_when_not_started = cls._patched_when_not.start()
# force requires to rerun the mock_hook decorator:
# try except is Python2/Python3 compatibility as Python3 has moved
# reload to importlib.
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
@classmethod
def tearDownClass(cls):
cls._patched_when.stop()
cls._patched_when_started = None
cls._patched_when = None
cls._patched_when_not.stop()
cls._patched_when_not_started = None
cls._patched_when_not = None
# and fix any breakage we did to the module
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
def setUp(self):
self._patches = {}
self._patches_start = {}
def tearDown(self):
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch(self, obj, attr, return_value=None, side_effect=None):
mocked = mock.patch.object(obj, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
started.side_effect = side_effect
self._patches_start[attr] = started
setattr(self, attr, started)
def test_registered_hooks(self):
# test that the hooks actually registered the relation expressions that
# are meaningful for this interface: this is to handle regressions.
# The keys are the function names that the hook attaches to.
when_patterns = {
}
when_not_patterns = {
}
# check the when hooks are attached to the expected functions
for t, p in [
(_when_args, when_patterns),
(_when_not_args, when_not_patterns),
]:
for f, args in t.items():
# check that function is in patterns
self.assertTrue(f in p.keys(), "{} not found".format(f))
# check that the lists are equal
lists = []
for a in args:
lists += a["args"][:]
self.assertEqual(
sorted(lists),
sorted(p[f]),
"{}: incorrect state registration".format(f),
)