Merge ~freyes/charm-grafana:bug/1872682 into charm-grafana:master

Proposed by Felipe Reyes
Status: Superseded
Proposed branch: ~freyes/charm-grafana:bug/1872682
Merge into: charm-grafana:master
Diff against target: 982 lines (+454/-40) (has conflicts)
24 files modified
src/actions/create-user (+14/-6)
src/files/dashboards_backup (+4/-1)
src/layer.yaml (+8/-1)
src/lib/charms/layer/grafana.py (+33/-3)
src/reactive/grafana.py (+137/-15)
src/requirements.txt (+1/-0)
src/templates/grafana.ini.j2 (+7/-1)
src/templates/juju-dashboards-backup.j2 (+1/-1)
src/templates/sync-grafana-snap (+7/-0)
src/tests/functional/requirements.txt (+6/-0)
src/tests/functional/tests/bundles/bionic-snap-tls.yaml (+1/-0)
src/tests/functional/tests/bundles/bionic-tls.yaml (+1/-0)
src/tests/functional/tests/bundles/focal-snap-tls.yaml (+1/-0)
src/tests/functional/tests/bundles/focal-tls.yaml (+1/-0)
src/tests/functional/tests/bundles/overlays/bionic-snap-tls.yaml.j2 (+11/-0)
src/tests/functional/tests/bundles/overlays/bionic-tls.yaml.j2 (+10/-0)
src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 (+32/-0)
src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 (+31/-0)
src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 (+12/-0)
src/tests/functional/tests/bundles/xenial-tls.yaml (+1/-0)
src/tests/functional/tests/test_grafana.py (+76/-12)
src/tests/functional/tests/tests.yaml (+7/-0)
src/tests/unit/test_grafana.py (+48/-0)
src/tox.ini (+4/-0)
Conflict in src/tests/functional/requirements.txt
Conflict in src/tox.ini
Reviewer Review Type Date Requested Status
Liam Young (community) Needs Fixing
Alvaro Uria (community) Needs Fixing
BootStack Reviewers Pending
BootStack Reviewers Pending
Review via email: mp+397611@code.launchpad.net

This proposal has been superseded by a proposal from 2021-08-06.

Commit message

Add tls-client layer to support HTTPS

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Felipe Reyes (freyes) wrote :

I've been trying to run the full functional tests (make functional) without success, I'm getting consistently the following error now:

unit-prometheus-0: 15:56:11 WARNING unit.prometheus/0.install subprocess.CalledProcessError: Command '['snap', 'refresh', '--amend', '--channel=stable', 'core']' returned non-zero exit status 1.

a few hours ago the charmstore was giving "error 500" for certain charms (e.g. easyrsa). I will post the results here once I get to full successful run.

Revision history for this message
Drew Freiberger (afreiberger) wrote :

we very much have had that problem throughout the release cycle. it's just a poke and hope game until snapstore does the right thing.

Revision history for this message
Felipe Reyes (freyes) wrote :

I was running the functional test suite and got into this race condition:

2021-02-11 20:32:54 INFO juju-log certificates:21: Restarting grafana-server
2021-02-11 20:32:54 INFO juju-log certificates:21: Invoking reactive handler: reactive/grafana.py:625:configure_sources
2021-02-11 20:32:54 INFO juju-log certificates:21: Found datasource: {'service_name': 'prometheus', 'type': 'prometheus', 'url': 'http://10.5.0.99:9090', 'description': 'Juju generated source
'}
2021-02-11 20:32:54 INFO juju-log certificates:21: Datasource already exist, updating: prometheus - Juju generated source
2021-02-11 20:32:54 ERROR juju-log certificates:21: Hook error:
...
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPSConnection object at 0x7f5e101bcca0>: Failed to establish a new connection: [Errno 111] Connection refused
...
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='127.0.0.1', port=3000): Max retries exceeded with url: /api/search?type=dash-db (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f5e101bcca0>: Failed to establish a new connection: [Errno 111] Connection refused'))

So it seems that the restart of the service is not blocking until it's available to serve requests. I will propose a different patch to improve this.

Revision history for this message
Alvaro Uria (aluria) wrote :

Please find a comment inline (re: tls_client.certs.changed decorated handler). Other than that, code review looks good. I'll run all tests and share the output here.

Revision history for this message
Alvaro Uria (aluria) wrote :

* make lint failed with:
./tests/unit/test_grafana.py:161:1: D102 Missing docstring in public method

* make unittests failed with:
tests/unit/test_grafana.py::GrafanaTestCase::test_request_certificate FAILED

See output at: https://pastebin.ubuntu.com/p/KsMPg5gHMm/

* make functional failed on the first bundle (tests/bundles/focal.yaml)
2021-03-19 07:31:39 [ERROR] unit-grafana-0.log: 2021-03-19 07:31:37 WARNING install Exception: port 3000 is closed

Revision history for this message
Alvaro Uria (aluria) wrote :

Added comment inline re: retry_on_exception not having a delay

review: Needs Fixing
Revision history for this message
Felipe Reyes (freyes) wrote :

On Fri, 2021-03-19 at 08:29 +0000, Alvaro Uria wrote:
> * make lint failed with:
> ./tests/unit/test_grafana.py:161:1: D102 Missing docstring in public
> method
>
> * make unittests failed with:
> tests/unit/test_grafana.py::GrafanaTestCase::test_request_certificate
> FAILED
>
> See output at: https://pastebin.ubuntu.com/p/KsMPg5gHMm/
>
> * make functional failed on the first bundle
> (tests/bundles/focal.yaml)
> 2021-03-19 07:31:39 [ERROR] unit-grafana-0.log: 2021-03-19 07:31:37
> WARNING install Exception: port 3000 is closed

This failure happens because there was no base_delay as you pointed out
in the retry_on_exception().

>
--
Felipe Reyes
Software Sustaining Engineer @ Canonical
# Email: <email address hidden> (GPG:0x9B1FFF39)
# Launchpad: ~freyes | IRC: freyes

Revision history for this message
Felipe Reyes (freyes) wrote :

Hi Alvaro, I just pushed a new version of the patch. make lint/unittests are OK, I'm running make functional now, but it will take a while :-) . Thanks for reviewing my patch.

Revision history for this message
Felipe Reyes (freyes) wrote :

Has anyone been able to run a successful run of grafana functional testing recently?, not necessarily with this patch, but in general, I get different issues all the time (snap store, or the charm store, or stsstack, or just juju timing out)

Revision history for this message
Xav Paice (xavpaice) wrote :

Functest run from the candidate/21.04 branch (a copy of master at the time of writing) https://pastebin.canonical.com/p/j8VxbzYNvc/ -> passed for the first bundle. Because I'm using Openstack as a cloud for the tests, latest versions of Zaza require python-openstackclient in order to complete the teardown of the model, and that means my tests only run for the first bundle. I'll look at a separate fix for this (for any of the charms running zaza) and then we can re-test here.

Tests on this branch also passed functests on my environment for the first bundle, however there are a LOT of test runs to complete for this and without automation of the tests this will cause a delay since we're locked into a release cycle right now.

Once this is reviewed and merged to master, it'll be available on cs:~llama-charmers-next/openstack-service-checks in any case.

Revision history for this message
Xav Paice (xavpaice) wrote :

Functest failed with:

2021-04-13 10:53:55 [ERROR] {'model_apt_install': 'zaza-2010f9813ac7'}
2021-04-13 10:53:55 [ERROR] Model model_apt_install (zaza-2010f9813ac7)
Traceback (most recent call last):
  File "/home/xav/charms/charm-grafana/src/.tox/func/bin/functest-run-suite", line 8, in <module>
    sys.exit(main())
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/charm_lifecycle/func_test_runner.py", line 308, in main
    func_test_runner(
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/charm_lifecycle/func_test_runner.py", line 242, in func_test_runner
    run_env_deployment(env_deployment, keep_model=preserve_model,
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/charm_lifecycle/func_test_runner.py", line 122, in run_env_deployment
    deploy.deploy(
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/charm_lifecycle/deploy.py", line 385, in deploy
    zaza.model.wait_for_application_states(
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 77, in _wrapper
    return run(_run_it())
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 62, in run
    return task.result()
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/__init__.py", line 76, in _run_it
    return await f(*args, **kwargs)
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/zaza/model.py", line 1167, in async_wait_for_application_states
    await model.block_until(
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/juju/model.py", line 727, in block_until
    await utils.block_until(done,
  File "/home/xav/charms/charm-grafana/src/.tox/func/lib/python3.8/site-packages/juju/utils.py", line 87, in block_until
    await asyncio.wait_for(_block(), timeout, loop=loop)
  File "/usr/lib/python3.8/asyncio/tasks.py", line 490, in wait_for
    raise exceptions.TimeoutError()
asyncio.exceptions.TimeoutError

Bundle it was deploying: src/tests/functional/tests/bundles/focal-tls.yaml

Revision history for this message
Liam Young (gnuoy) wrote :

I've raised a PR against freyes's branch which includes fixes to the TimeoutError mentioned in the previous comment and the dependency on OpenStack clients mentioned before that.

https://code.launchpad.net/~gnuoy/charm-grafana/+git/charm-grafana/+merge/404961

Revision history for this message
James Troup (elmo) wrote :

Marking this as rejected to get it out of the review queue. The work done on this branch landed with Liam's MP. Thanks Felipe and Liam!

Revision history for this message
Felipe Reyes (freyes) wrote :

Hi James, if by "Liam's MP" you mean https://code.launchpad.net/~gnuoy/charm-grafana/+git/charm-grafana/+merge/404961 , that one was proposed for merge in my branch[0] and it's already merged. My branch was just rebased and pushed it for review.

Is there some other MP in flight or pending to be created and I'm confusing things?

Best,

PS: I'm running "make functional"at the moment, still running, but not failures so far[1]

[0]Proposed branch: ~gnuoy/charm-grafana:bug/1872682-fixes
Merge into: ~freyes/charm-grafana:bug/1872682

[1] $ grep "Deploying bundle" functional.log
2021-08-04 23:03:50 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/focal.yaml' on to 'zaza-985edaed9dda' model
2021-08-04 23:27:32 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/focal-tls.yaml' on to 'zaza-4fa38806714d' model
2021-08-04 23:51:24 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/bionic.yaml' on to 'zaza-7c7752252403' model
2021-08-05 00:14:42 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/bionic-tls.yaml' on to 'zaza-763edc3c044f' model
2021-08-05 00:35:21 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/xenial.yaml' on to 'zaza-4ed0f8e6798e' model
2021-08-05 00:57:14 [INFO] Deploying bundle '/home/freyes/Projects/charms/grafana-charm/src/tests/functional/tests/bundles/xenial-tls.yaml' on to 'zaza-e26f68ffd00e' model

Revision history for this message
Liam Young (gnuoy) wrote :

Looks like src/tox.ini needs fixing

review: Needs Fixing
Revision history for this message
Felipe Reyes (freyes) wrote :

Weird, I'm not seeing that merge conflict locally, will give it a try in a fresh git clone.

re: functional tests, they were running fine, but due to a networking issue the full run couldn't complete - https://paste.ubuntu.com/p/3wvHsZsg8f/ - running again from a different node to avoid relying on the VPN.

Revision history for this message
Felipe Reyes (freyes) wrote :
Download full text (3.8 KiB)

This is merge of my branch into master -> https://paste.ubuntu.com/p/QHqcTykB8t/ , I don't see changes to src/tox.ini (nor merge conflicts), the change that is showing Launchpad existed in a previous version of the patch (before the rebase I did yesterday), so I wonder if there is a caching issue since this Merge Proposal is in "rejected" state.

ubuntu@freyes-bastion:~$ git clone -b master https://git.launchpad.net/charm-grafana
Cloning into 'charm-grafana'...
remote: Enumerating objects: 1757, done.
remote: Counting objects: 100% (1757/1757), done.
remote: Compressing objects: 100% (1009/1009), done.
remote: Total 1757 (delta 882), reused 902 (delta 469)
Receiving objects: 100% (1757/1757), 309.80 KiB | 433.00 KiB/s, done.
Resolving deltas: 100% (882/882), done.
ubuntu@freyes-bastion:~$ cd charm-grafana/
ubuntu@freyes-bastion:~/charm-grafana$ git remote -v
origin https://git.launchpad.net/charm-grafana (fetch)
origin https://git.launchpad.net/charm-grafana (push)
ubuntu@freyes-bastion:~/charm-grafana$ git remote add freyes https://git.launchpad.net/~freyes/charm-grafana
ubuntu@freyes-bastion:~/charm-grafana$ git fetch freyes
remote: Enumerating objects: 84, done.
remote: Counting objects: 100% (84/84), done.
 * [new branch] bug/1872682 -> freyes/bug/1872682
 * [new branch] bug/1893137 -> freyes/bug/1893137
ubuntu@freyes-bastion:~/charm-grafana$ git checkout -b bug/1872682 --track freyes/bug/1872682
Branch 'bug/1872682' set up to track remote branch 'bug/1872682' from 'freyes'.
Switched to a new branch 'bug/1872682'
ubuntu@freyes-bastion:~/charm-grafana$ git branch
* bug/1872682
  master
ubuntu@freyes-bastion:~/charm-grafana$ git merge --no-ff --no-commit bug/1872682
Already up to date.
ubuntu@freyes-bastion:~/charm-grafana$ git status
On branch bug/1872682
Your branch is up to date with 'freyes/bug/1872682'.

nothing to commit, working tree clean
ubuntu@freyes-bastion:~/charm-grafana$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
ubuntu@freyes-bastion:~/charm-grafana$ git merge --no-ff --no-commit bug/1872682
Automatic merge went well; stopped before committing as requested
ubuntu@freyes-bastion:~/charm-grafana$ git status
On branch master
Your branch is up to date with 'origin/master'.

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
        modified: src/actions/create-user
        modified: src/files/dashboards_backup
        modified: src/layer.yaml
        modified: src/lib/charms/layer/grafana.py
        modified: src/reactive/grafana.py
        modified: src/requirements.txt
        modified: src/templates/grafana.ini.j2
        modified: src/templates/juju-dashboards-backup.j2
        new file: src/templates/sync-grafana-snap
        new file: src/tests/functional/tests/bundles/bionic-snap-tls.yaml
        new file: src/tests/functional/tests/bundles/bionic-tls.yaml
        new file: src/tests/functional/tests/bundles/focal-snap-tls.yaml
        new file: src/tests/functional/tests/bundles/focal-tls.yaml
        new file: src/tests/functional/tests/bundles/overlays/bionic-snap...

Read more...

Revision history for this message
Felipe Reyes (freyes) wrote :

I could get a full run of "make functional" succeed, to achieve this I had to fix a race condition on the test_13_grafana_dashboard_backup()[0] test.

summary of the tests execution:

2021-08-06 02:37:18 [INFO] Events:
  Deploy Bundle:
    Start: 1628230469.9741626
    Finish: 1628230580.8524628
    Elapsed Time: 110.87830018997192
    PCT Of Run Time: 2
  Prepare Environment:
    Start: 1628230458.4041638
    Finish: 1628230469.9739351
    Elapsed Time: 11.56977128982544
    PCT Of Run Time: 1
  Test tests.test_grafana.AptGrafanaTest:
    Start: 1628231751.6462507
    Finish: 1628231776.0118518
    Elapsed Time: 24.365601062774658
    PCT Of Run Time: 1
  Test tests.test_grafana.CharmOperationTest:
    Start: 1628231214.4372096
    Finish: 1628231751.6461709
    Elapsed Time: 537.2089612483978
    PCT Of Run Time: 6
  Test tests.test_grafana.SnappedGrafanaTest:
    Start: 1628222783.1681652
    Finish: 1628222887.3098197
    Elapsed Time: 104.14165449142456
    PCT Of Run Time: 2
  Wait for Deployment:
    Start: 1628230580.8525686
    Finish: 1628231212.1852653
    Elapsed Time: 631.3326966762543
    PCT Of Run Time: 8
Metadata: {}

Full output can be found at https://paste.ubuntu.com/p/79nwNB6TKr/

[0] https://git.launchpad.net/~freyes/charm-grafana/commit/?id=229263cbaf7ddc85d1f84a34c15dfed1c5e547a4

Revision history for this message
Felipe Reyes (freyes) wrote :

I will re-submit my proposal, the commit ids shown in the "unmerged commits" are invalid

Unmerged commits in Launchpad

6e52a25... by Liam Young on 2021-06-30 Use easyrsa on bionic for xenial tests
5ca8ed8... by Liam Young on 2021-06-30 Bug fixes for enabling TLS
4c7b77b... by Felipe Reyes on 2021-02-05 Add tls-client layer to support HTTPS

versus

$ git log --oneline
229263c (HEAD -> bug/1872682, freyes/bug/1872682) Wait until /etc/cron.d/juju-dashboards-backup appears
817ed45 Replace assertTrue with assertIn, assertNotIn and assertEqual
7108c49 Use easyrsa on bionic for xenial tests
ef9711e Bug fixes for enabling TLS
c9d57f8 Add tls-client layer to support HTTPS

Unmerged commits

6e52a25... by Liam Young

Use easyrsa on bionic for xenial tests

Use easyrsa on bionic for xenial tests as easyrsa is currently
failing to install on xenial.

5ca8ed8... by Liam Young

Bug fixes for enabling TLS

* Update create-user action to work with https
* Update backup to work with https
* Update functional test requirements to work-around zaza bug *1
* Update tests.yaml to expect easyrsa to come up with a workload
  status message of 'Certificate Authority connected.'
* Do not use f strings as the charm needs to run on xenial.

*1 https://github.com/openstack-charmers/zaza/issues/452

4c7b77b... by Felipe Reyes

Add tls-client layer to support HTTPS

This patch adds a new interface provided by the tls-client later. When related
to a certificates provider (e.g. EasyRSA) it will configure the daemon to
serve over HTTPS.

Fixes-Bug: #1893137

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/actions/create-user b/src/actions/create-user
index ab9f96c..916534c 100755
--- a/src/actions/create-user
+++ b/src/actions/create-user
@@ -12,18 +12,24 @@ from charmhelpers.core.hookenv import (
12 log,12 log,
13)13)
1414
15from charms.layer.grafana import get_admin_password15from charms.layer.grafana import (
16 get_admin_password,
17 get_protocol,
18)
1619
17action = "create-user"20action = "create-user"
1821
19admin_passwd = get_admin_password()22admin_passwd = get_admin_password()
2023
24# Talking to ourselves on localhost so don't worry about CAs
25verify_ca = False
26
21if admin_passwd is None:27if admin_passwd is None:
22 action_fail('Unable to retrieve password.')28 action_fail('Unable to retrieve password.')
23 sys.exit(0)29 sys.exit(0)
2430
25port = config('port')31port = config('port')
26grafana = "http://localhost:%s" % (port)32grafana = "{}://localhost:{}".format(get_protocol(), port)
27api_auth = ('admin', admin_passwd)33api_auth = ('admin', admin_passwd)
2834
29# http://docs.grafana.org/http_api/admin/#global-users35# http://docs.grafana.org/http_api/admin/#global-users
@@ -57,13 +63,13 @@ user_data = {
57headers = {'Content-Type': 'application/json'}63headers = {'Content-Type': 'application/json'}
5864
59grafana_api_create_user_url = grafana + api_create_user_url65grafana_api_create_user_url = grafana + api_create_user_url
60
61if requests.utils.urlparse(grafana_api_create_user_url).scheme:66if requests.utils.urlparse(grafana_api_create_user_url).scheme:
62 r_create = requests.post(67 r_create = requests.post(
63 grafana_api_create_user_url,68 grafana_api_create_user_url,
64 auth=api_auth,69 auth=api_auth,
65 headers=headers,70 headers=headers,
66 data=json.dumps(user_data)71 data=json.dumps(user_data),
72 verify=verify_ca,
67 )73 )
68else:74else:
69 action_fail("Grafana url %s failed to parse!" % (grafana_api_create_user_url))75 action_fail("Grafana url %s failed to parse!" % (grafana_api_create_user_url))
@@ -90,7 +96,8 @@ grafana_api_org_url = grafana + api_org_url
90if requests.utils.urlparse(grafana_api_org_url).scheme:96if requests.utils.urlparse(grafana_api_org_url).scheme:
91 r_org = requests.get(97 r_org = requests.get(
92 grafana_api_org_url,98 grafana_api_org_url,
93 auth=api_auth99 auth=api_auth,
100 verify=verify_ca
94 )101 )
95else:102else:
96 action_fail("Grafana url %s failed to parse" % (grafana_api_org_url))103 action_fail("Grafana url %s failed to parse" % (grafana_api_org_url))
@@ -108,7 +115,8 @@ if r_org.status_code == 200:
108 grafana_api_org_user_url,115 grafana_api_org_user_url,
109 auth=api_auth,116 auth=api_auth,
110 headers=headers,117 headers=headers,
111 data=json.dumps(org_user_data)118 data=json.dumps(org_user_data),
119 verify=verify_ca
112 )120 )
113 else:121 else:
114 action_fail("Grafana url %s failed to parse" % (grafana_api_org_user_url))122 action_fail("Grafana url %s failed to parse" % (grafana_api_org_user_url))
diff --git a/src/files/dashboards_backup b/src/files/dashboards_backup
index 30299a2..88c6310 100755
--- a/src/files/dashboards_backup
+++ b/src/files/dashboards_backup
@@ -18,7 +18,7 @@ def get_backup_filename(directory, org_name, uri):
1818
1919
20def main(args):20def main(args):
21 base_url = 'http://localhost:{port}/api/'.format(port=args.port)21 base_url = '{scheme}://localhost:{port}/api/'.format(scheme=args.scheme, port=args.port)
22 for key in args.api_keys:22 for key in args.api_keys:
23 headers = {'Authorization': 'Bearer {}'.format(key)}23 headers = {'Authorization': 'Bearer {}'.format(key)}
24 org_name = requests.get(base_url+'org', headers=headers).json()['name']24 org_name = requests.get(base_url+'org', headers=headers).json()['name']
@@ -38,6 +38,9 @@ if __name__ == '__main__':
38 parser.add_argument('-d', '--directory', help='Directory where to store backups',38 parser.add_argument('-d', '--directory', help='Directory where to store backups',
39 default='/srv/backups')39 default='/srv/backups')
40 parser.add_argument('-p', '--port', help='Port to access grafana API', default='3000')40 parser.add_argument('-p', '--port', help='Port to access grafana API', default='3000')
41 parser.add_argument('-s', '--scheme',
42 help='Scheme to use to access grafana API e.g. http or https',
43 default='http')
41 parser.add_argument('api_keys', help='List of API keys to use for backups', nargs='+')44 parser.add_argument('api_keys', help='List of API keys to use for backups', nargs='+')
42 args = parser.parse_args()45 args = parser.parse_args()
43 main(args)46 main(args)
diff --git a/src/layer.yaml b/src/layer.yaml
index 6e51d38..3ddcaa7 100644
--- a/src/layer.yaml
+++ b/src/layer.yaml
@@ -1,4 +1,11 @@
1includes: ['layer:basic', 'layer:snap', 'interface:nrpe-external-master', 'interface:grafana-source', 'interface:http', 'interface:grafana-dashboard']1includes:
2 - 'layer:basic'
3 - 'layer:snap'
4 - 'layer:tls-client'
5 - 'interface:nrpe-external-master'
6 - 'interface:grafana-source'
7 - 'interface:http'
8 - 'interface:grafana-dashboard'
2ignore: ['.*.swp' ]9ignore: ['.*.swp' ]
3options:10options:
4 basic:11 basic:
diff --git a/src/lib/charms/layer/grafana.py b/src/lib/charms/layer/grafana.py
index d8fc7b8..0acb980 100644
--- a/src/lib/charms/layer/grafana.py
+++ b/src/lib/charms/layer/grafana.py
@@ -15,10 +15,19 @@ from charmhelpers.core.hookenv import (
15)15)
1616
17from charms.layer import snap17from charms.layer import snap
18from charms.reactive.flags import is_flag_set
1819
19import requests20import requests
2021
2122
23# When using 'certifi' from the virtualenv, the system-wide certificates store
24# is not used, so installed certificates won't be used to validate hosts.
25# Adding the system CA bundle
26# https://git.launchpad.net/ubuntu/+source/python-certifi/tree/debian/patches/0001-Use-Debian-provided-etc-ssl-certs-ca-certificates.cr.patch
27SYSTEM_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"
28CA_CERT_PATH = "/var/snap/grafana/common/ssl/ca-certificates.crt"
29
30
22class ChangeStatus(enum.Enum):31class ChangeStatus(enum.Enum):
23 """Model Snap channel states."""32 """Model Snap channel states."""
2433
@@ -42,6 +51,22 @@ def get_admin_password():
42 return config("admin_password") or unitdata.kv().get("grafana.admin_password")51 return config("admin_password") or unitdata.kv().get("grafana.admin_password")
4352
4453
54def get_protocol():
55 """Check if the SSL certificates were configured and return http or https."""
56 if is_flag_set("grafana.certificates.configured"):
57 return "https"
58 else:
59 return "http"
60
61
62def get_ca_cert_path():
63 """Return the path where the CA certificates store is."""
64 if config("install_method") == "snap":
65 return CA_CERT_PATH
66 else:
67 return SYSTEM_CA_BUNDLE
68
69
45def compute_dash_title(title, remote_app=None):70def compute_dash_title(title, remote_app=None):
46 """Compute title for dashboards.71 """Compute title for dashboards.
4772
@@ -80,9 +105,10 @@ def compute_dash_title(title, remote_app=None):
80def get_folders():105def get_folders():
81 """Retrieve all folders."""106 """Retrieve all folders."""
82 r = requests.get(107 r = requests.get(
83 "http://localhost:{}/api/folders".format(config("port")),108 "{}://localhost:{}/api/folders".format(get_protocol(), config("port")),
84 auth=("admin", get_admin_password()),109 auth=("admin", get_admin_password()),
85 headers={"Accept": "application/json"},110 headers={"Accept": "application/json"},
111 verify=get_ca_cert_path(),
86 )112 )
87 r.raise_for_status()113 r.raise_for_status()
88 folders = r.json()114 folders = r.json()
@@ -93,10 +119,11 @@ def get_folders():
93def create_folder(folder_name):119def create_folder(folder_name):
94 """Create a folder through Grafana API."""120 """Create a folder through Grafana API."""
95 r = requests.post(121 r = requests.post(
96 "http://localhost:{}/api/folders".format(config("port")),122 "{}://localhost:{}/api/folders".format(get_protocol(), config("port")),
97 auth=("admin", get_admin_password()),123 auth=("admin", get_admin_password()),
98 headers={"Accept": "application/json"},124 headers={"Accept": "application/json"},
99 data={"title": folder_name},125 data={"title": folder_name},
126 verify=get_ca_cert_path(),
100 )127 )
101 r.raise_for_status()128 r.raise_for_status()
102 folder = r.json()129 folder = r.json()
@@ -129,7 +156,9 @@ def ensure_and_get_dash_folder(remote_model):
129def post_dashboard(name, dashboard):156def post_dashboard(name, dashboard):
130 """Upload a dashboard to Grafana."""157 """Upload a dashboard to Grafana."""
131 headers = {"Content-Type": "application/json"}158 headers = {"Content-Type": "application/json"}
132 import_url = "http://localhost:{}/api/dashboards/db".format(config("port"))159 import_url = "{}://localhost:{}/api/dashboards/db".format(
160 get_protocol(), config("port")
161 )
133 passwd = get_admin_password()162 passwd = get_admin_password()
134 if passwd is None:163 if passwd is None:
135 return (False, "Unable to retrieve grafana password.")164 return (False, "Unable to retrieve grafana password.")
@@ -139,6 +168,7 @@ def post_dashboard(name, dashboard):
139 auth=api_auth,168 auth=api_auth,
140 headers=headers,169 headers=headers,
141 data=json.dumps(dashboard),170 data=json.dumps(dashboard),
171 verify=get_ca_cert_path(),
142 )172 )
143 if r.status_code == 200:173 if r.status_code == 200:
144 return (True, None)174 return (True, None)
diff --git a/src/reactive/grafana.py b/src/reactive/grafana.py
index 37958d3..10a1656 100644
--- a/src/reactive/grafana.py
+++ b/src/reactive/grafana.py
@@ -68,10 +68,13 @@ wipe_nrpe_checks (no nrpe-external-master.available)
68import base6468import base64
69import datetime69import datetime
70import glob70import glob
71import grp
71import json72import json
72import os73import os
74import pwd
73import re75import re
74import shutil76import shutil
77import socket
75import subprocess78import subprocess
76import time79import time
7780
@@ -80,18 +83,23 @@ from charmhelpers.contrib.charmsupport import nrpe
80from charmhelpers.core import (83from charmhelpers.core import (
81 hookenv,84 hookenv,
82 host,85 host,
86 templating,
83 unitdata,87 unitdata,
84)88)
89from charmhelpers.core.decorators import retry_on_exception
85from charmhelpers.core.templating import render90from charmhelpers.core.templating import render
8691
87from charms.layer import snap92from charms.layer import snap, tls_client
88from charms.layer.grafana import (93from charms.layer.grafana import (
94 CA_CERT_PATH,
89 ChangeStatus,95 ChangeStatus,
90 check_snap_channel,96 check_snap_channel,
91 download_file,97 download_file,
92 get_admin_password,98 get_admin_password,
99 get_ca_cert_path,
93 get_deb_package_version,100 get_deb_package_version,
94 get_installed_package_version,101 get_installed_package_version,
102 get_protocol,
95 import_dashboard,103 import_dashboard,
96)104)
97from charms.reactive import (105from charms.reactive import (
@@ -102,6 +110,7 @@ from charms.reactive import (
102 when_any,110 when_any,
103 when_not,111 when_not,
104)112)
113from charms.reactive.flags import is_flag_set
105from charms.reactive.helpers import (114from charms.reactive.helpers import (
106 any_file_changed,115 any_file_changed,
107 is_state,116 is_state,
@@ -119,8 +128,16 @@ import six
119SVCNAME = {"snap": "snap.grafana.grafana", "apt": "grafana-server"}128SVCNAME = {"snap": "snap.grafana.grafana", "apt": "grafana-server"}
120SNAP_NAME = "grafana"129SNAP_NAME = "grafana"
121SNAP_DATA = "/var/snap/{}/current".format(SNAP_NAME)130SNAP_DATA = "/var/snap/{}/current".format(SNAP_NAME)
122SNAP_COMMON = "/var/snap/{}/common/data".format(SNAP_NAME)131SNAP_COMMON_DIR = "/var/snap/{}/common".format(SNAP_NAME)
123132SNAP_COMMON_DATA = "{}/data".format(SNAP_COMMON_DIR)
133CERT_PATH = {
134 "snap": "{}/ssl/server.crt".format(SNAP_COMMON_DIR),
135 "apt": "/etc/grafana/ssl/server.crt",
136}
137CERT_KEY_PATH = {
138 "snap": "{}/ssl/server.key".format(SNAP_COMMON_DIR),
139 "apt": "/etc/grafana/ssl/server.key",
140}
124GRAFANA_INI = {141GRAFANA_INI = {
125 "snap": "{}/conf/grafana.ini".format(SNAP_DATA),142 "snap": "{}/conf/grafana.ini".format(SNAP_DATA),
126 "apt": "/etc/grafana/grafana.ini",143 "apt": "/etc/grafana/grafana.ini",
@@ -129,6 +146,7 @@ GRAFANA_INI_TMPL = "grafana.ini.j2"
129GRAFANA_DEPS = ["libfontconfig1"]146GRAFANA_DEPS = ["libfontconfig1"]
130DASHBOARDS_BACKUP_CRON = "/etc/cron.d/juju-dashboards-backup"147DASHBOARDS_BACKUP_CRON = "/etc/cron.d/juju-dashboards-backup"
131DASHBOARDS_BACKUP_CRON_TMPL = "juju-dashboards-backup.j2"148DASHBOARDS_BACKUP_CRON_TMPL = "juju-dashboards-backup.j2"
149CA_CERTIFICATES_HOOK = "/etc/ca-certificates/update.d/sync-grafana-snap"
132150
133151
134try:152try:
@@ -233,7 +251,7 @@ def install_packages():
233251
234def data_path():252def data_path():
235 """Retrieve data store depending on install method."""253 """Retrieve data store depending on install method."""
236 data_dir = {"snap": SNAP_COMMON, "apt": "/var/lib/grafana"}254 data_dir = {"snap": SNAP_COMMON_DATA, "apt": "/var/lib/grafana"}
237 source = get_install_source()255 source = get_install_source()
238 if not source:256 if not source:
239 remove_state("grafana.installed")257 remove_state("grafana.installed")
@@ -460,7 +478,13 @@ def setup_grafana():
460 grafana_ini = GRAFANA_INI[source]478 grafana_ini = GRAFANA_INI[source]
461 hookenv.status_set("maintenance", "Configuring grafana")479 hookenv.status_set("maintenance", "Configuring grafana")
462 config = hookenv.config()480 config = hookenv.config()
463 settings = {"config": config}481 settings = {
482 "config": config,
483 "protocol": get_protocol(),
484 "cert_file": CERT_PATH[config["install_method"]],
485 "cert_key": CERT_KEY_PATH[config["install_method"]],
486 }
487
464 smtp_auth = config.get("smtp_auth", False)488 smtp_auth = config.get("smtp_auth", False)
465 if smtp_auth and len(smtp_auth.split(":")) == 2:489 if smtp_auth and len(smtp_auth.split(":")) == 2:
466 settings["smtp_user"] = smtp_auth.split(":")[0]490 settings["smtp_user"] = smtp_auth.split(":")[0]
@@ -501,6 +525,7 @@ def setup_backup_shedule():
501 "directory": config.get("dashboards_backup_dir"),525 "directory": config.get("dashboards_backup_dir"),
502 "port": config.get("port"),526 "port": config.get("port"),
503 "backup_keys": " ".join(add_backup_api_keys()),527 "backup_keys": " ".join(add_backup_api_keys()),
528 "scheme": get_protocol(),
504 }529 }
505 render(530 render(
506 source=DASHBOARDS_BACKUP_CRON_TMPL,531 source=DASHBOARDS_BACKUP_CRON_TMPL,
@@ -536,6 +561,8 @@ def restart_grafana():
536 hookenv.log(msg)561 hookenv.log(msg)
537 hookenv.status_set("maintenance", msg)562 hookenv.status_set("maintenance", msg)
538 host.service_restart(svcname)563 host.service_restart(svcname)
564
565 _block_until_port_open()
539 hookenv.status_set("active", "Ready")566 hookenv.status_set("active", "Ready")
540 set_state("grafana.started")567 set_state("grafana.started")
541 hookenv.status_set("active", "Started {}".format(svcname))568 hookenv.status_set("active", "Started {}".format(svcname))
@@ -562,11 +589,18 @@ def update_nrpe_config():
562 nrpe.add_init_service_checks(nrpe_setup, [svcname], current_unit)589 nrpe.add_init_service_checks(nrpe_setup, [svcname], current_unit)
563590
564 # write the http check591 # write the http check
565 nrpe_setup.add_check(592 if get_protocol() == "https":
566 "grafana_http",593 nrpe_setup.add_check(
567 "Grafana HTTP check",594 "grafana_http",
568 "check_http -I 127.0.0.1 -p {} -u /login".format(config["port"]),595 "Grafana HTTPS check",
569 )596 "check_http -S -I 127.0.0.1 -p {} -u /login".format(config["port"]),
597 )
598 else:
599 nrpe_setup.add_check(
600 "grafana_http",
601 "Grafana HTTP check",
602 "check_http -I 127.0.0.1 -p {} -u /login".format(config["port"]),
603 )
570604
571 nrpe_setup.write()605 nrpe_setup.write()
572 set_state("grafana.nagios-setup.completed")606 set_state("grafana.nagios-setup.completed")
@@ -747,8 +781,9 @@ def get_current_dashboards(port, passwd):
747 https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/781 https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/
748 """782 """
749 dash_req = requests.get(783 dash_req = requests.get(
750 "http://127.0.0.1:{}/api/search?type=dash-db".format(port),784 "{}://127.0.0.1:{}/api/search?type=dash-db".format(get_protocol(), port),
751 auth=("admin", passwd),785 auth=("admin", passwd),
786 verify=get_ca_cert_path(),
752 )787 )
753 return dash_req.json() if dash_req.status_code == 200 else []788 return dash_req.json() if dash_req.status_code == 200 else []
754789
@@ -766,8 +801,9 @@ def get_current_dashboard_json(uid, port, passwd):
766 return default_dashboard801 return default_dashboard
767802
768 dash_req = requests.get(803 dash_req = requests.get(
769 "http://127.0.0.1:{}/api/dashboards/uid/{}".format(port, uid),804 "{}://127.0.0.1:{}/api/dashboards/uid/{}".format(get_protocol(), port, uid),
770 auth=("admin", passwd),805 auth=("admin", passwd),
806 verify=get_ca_cert_path(),
771 )807 )
772 if dash_req.status_code != 200:808 if dash_req.status_code != 200:
773 return default_dashboard809 return default_dashboard
@@ -856,8 +892,13 @@ def check_and_add_dashboard(
856 )892 )
857893
858 hookenv.log("Using Dashboard Template: {}".format(filename))894 hookenv.log("Using Dashboard Template: {}".format(filename))
859 post_req = "http://127.0.0.1:{}/api/dashboards/db".format(port)895 post_req = "{}://127.0.0.1:{}/api/dashboards/db".format(get_protocol(), port)
860 r = requests.post(post_req, json=dashboard_json, auth=("admin", gf_adminpasswd))896 r = requests.post(
897 post_req,
898 json=dashboard_json,
899 auth=("admin", gf_adminpasswd),
900 verify=get_ca_cert_path(),
901 )
861902
862 if r.status_code != 200:903 if r.status_code != 200:
863 hookenv.log(904 hookenv.log(
@@ -1075,8 +1116,9 @@ def get_orgs(port, passwd):
1075 https://grafana.com/docs/grafana/latest/http_api/org/1116 https://grafana.com/docs/grafana/latest/http_api/org/
1076 """1117 """
1077 req = requests.get(1118 req = requests.get(
1078 "http://127.0.0.1:{}/api/orgs".format(port),1119 "{}://127.0.0.1:{}/api/orgs".format(get_protocol(), port),
1079 auth=("admin", passwd),1120 auth=("admin", passwd),
1121 verify=get_ca_cert_path(),
1080 )1122 )
1081 return req.json() if req.status_code == 200 else []1123 return req.json() if req.status_code == 200 else []
10821124
@@ -1255,3 +1297,83 @@ def import_dashboards(dashboards):
1255 req.respond(success, reason)1297 req.respond(success, reason)
12561298
1257 kv.set("dash_digests", dashboard_digests)1299 kv.set("dash_digests", dashboard_digests)
1300
1301
1302@when("certificates.available")
1303@when_not("grafana.certificates.configured")
1304@when_not("grafana.certificates.requested")
1305def tls_request_certificate(tls):
1306 """Create a server certificate for this server."""
1307 # Use the public ip of this unit as the Common Name for the certificate.
1308 common_name = hookenv.unit_public_ip()
1309 # Get a list of Subject Alt Names for the certificate.
1310 sans = []
1311 sans.append(hookenv.unit_public_ip())
1312 sans.append(hookenv.unit_private_ip())
1313 sans.append(socket.gethostname())
1314 sans.append("127.0.0.1")
1315 sans.append("localhost")
1316 hookenv.log(
1317 "Requesting certificate with CN: {common_name}".format(common_name=common_name)
1318 )
1319 tls_client.request_server_cert(
1320 common_name,
1321 sans,
1322 crt_path=CERT_PATH[hookenv.config("install_method")],
1323 key_path=CERT_KEY_PATH[hookenv.config("install_method")],
1324 )
1325 _maybe_install_ca_certificates_hook()
1326 set_state("grafana.certificates.requested")
1327
1328
1329@when("tls_client.certs.changed")
1330@when("grafana.certificates.requested")
1331def tls_certificate_changed():
1332 """Request to reconfigure grafana after the certificates were written."""
1333 # the certificates are written in owned by root and 0o770 permissions
1334 # which prevents grafana-server process to read them when using the debian
1335 # package, so the parent directory needs to allow the access by 'grafana'
1336 # group.
1337 if hookenv.config("install_method") == "apt":
1338 uid = pwd.getpwnam("root").pw_uid
1339 gid = grp.getgrnam("grafana").gr_gid
1340 ssl_dir = os.path.dirname(CERT_PATH[hookenv.config("install_method")])
1341 os.chown(ssl_dir, uid, gid)
1342
1343 # making sure the hook to update the certificate inside the snap's common
1344 # dir is executed if needed.
1345 subprocess.check_call(["update-ca-certificates"])
1346
1347 set_state("grafana.certificates.configured")
1348 if is_flag_set("grafana.configured"):
1349 remove_state("grafana.configured")
1350
1351
1352def _maybe_install_ca_certificates_hook():
1353 # When grafana is running within a snap won't have access to the system's
1354 # certificates store, so a hook script is installed to rsync it on any
1355 # execution of update-ca-certificates
1356 if hookenv.config("install_method") != "snap":
1357 # when running from the deb package there is no need to install the
1358 # hook, the ca-certificates will take care of everything for us.
1359 return
1360
1361 templating.render(
1362 "sync-grafana-snap",
1363 CA_CERTIFICATES_HOOK,
1364 context={"CA_CERT_PATH": CA_CERT_PATH},
1365 perms=0o755,
1366 )
1367 subprocess.check_call(["update-ca-certificates"])
1368
1369
1370# Block for a not so small period of time, because in hyperconverged
1371# environments, specially while a bundle is being deployed, the amount of IOPS
1372# in flight may prevent restarts from happening fast enough.
1373@retry_on_exception(10, base_delay=2)
1374def _block_until_port_open():
1375 a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1376 port = hookenv.config("port")
1377 location = ("127.0.0.1", int(port))
1378 if a_socket.connect_ex(location) != 0:
1379 raise Exception("port %s is closed" % port)
diff --git a/src/requirements.txt b/src/requirements.txt
index 8462291..236ad65 100644
--- a/src/requirements.txt
+++ b/src/requirements.txt
@@ -1 +1,2 @@
1# Include python requirements here1# Include python requirements here
2rpdb
diff --git a/src/templates/grafana.ini.j2 b/src/templates/grafana.ini.j2
index 06278fc..77d4673 100644
--- a/src/templates/grafana.ini.j2
+++ b/src/templates/grafana.ini.j2
@@ -8,7 +8,13 @@ instance_name = {{ config.instance_name }}
88
9[server]9[server]
10# Protocol (http or https)10# Protocol (http or https)
11;protocol = http11protocol = {{ protocol }}
12
13{%- if protocol == "https" %}
14# https certs & key file
15cert_file = {{ cert_file }}
16cert_key = {{ cert_key }}
17{%- endif %}
1218
13# The ip address to bind to, empty will bind to all interfaces19# The ip address to bind to, empty will bind to all interfaces
14http_addr = 0.0.0.020http_addr = 0.0.0.0
diff --git a/src/templates/juju-dashboards-backup.j2 b/src/templates/juju-dashboards-backup.j2
index 00f0737..993c48a 100644
--- a/src/templates/juju-dashboards-backup.j2
+++ b/src/templates/juju-dashboards-backup.j2
@@ -1,5 +1,5 @@
1#1#
2# Please do not edit. Juju will overwrite it.2# Please do not edit. Juju will overwrite it.
3#3#
4{{ schedule }} root /usr/local/bin/dashboards_backup -d {{ directory }} -p {{ port }} {{ backup_keys }}4{{ schedule }} root /usr/local/bin/dashboards_backup -d {{ directory }} -p {{ port }} -s {{ scheme }} {{ backup_keys }}
55
diff --git a/src/templates/sync-grafana-snap b/src/templates/sync-grafana-snap
6new file mode 1006446new file mode 100644
index 0000000..7bf28b2
--- /dev/null
+++ b/src/templates/sync-grafana-snap
@@ -0,0 +1,7 @@
1#!/bin/sh
2#
3# This file is managed by juju.
4#
5
6set -e
7rsync /etc/ssl/certs/ca-certificates.crt {{ CA_CERT_PATH }}
diff --git a/src/tests/functional/requirements.txt b/src/tests/functional/requirements.txt
index bbe8435..a9af82d 100644
--- a/src/tests/functional/requirements.txt
+++ b/src/tests/functional/requirements.txt
@@ -1,2 +1,8 @@
1git+https://github.com/openstack-charmers/zaza.git#egg=zaza1git+https://github.com/openstack-charmers/zaza.git#egg=zaza
2<<<<<<< src/tests/functional/requirements.txt
2python-openstackclient3python-openstackclient
4=======
5# OpenStack tools current;y required when using OpenStack provider.
6python-keystoneclient
7python-novaclient
8>>>>>>> src/tests/functional/requirements.txt
diff --git a/src/tests/functional/tests/bundles/bionic-snap-tls.yaml b/src/tests/functional/tests/bundles/bionic-snap-tls.yaml
3new file mode 1200009new file mode 120000
index 0000000..f81f6ff
--- /dev/null
+++ b/src/tests/functional/tests/bundles/bionic-snap-tls.yaml
@@ -0,0 +1 @@
1base.yaml
0\ No newline at end of file2\ No newline at end of file
diff --git a/src/tests/functional/tests/bundles/bionic-tls.yaml b/src/tests/functional/tests/bundles/bionic-tls.yaml
1new file mode 1200003new file mode 120000
index 0000000..f81f6ff
--- /dev/null
+++ b/src/tests/functional/tests/bundles/bionic-tls.yaml
@@ -0,0 +1 @@
1base.yaml
0\ No newline at end of file2\ No newline at end of file
diff --git a/src/tests/functional/tests/bundles/focal-snap-tls.yaml b/src/tests/functional/tests/bundles/focal-snap-tls.yaml
1new file mode 1200003new file mode 120000
index 0000000..f81f6ff
--- /dev/null
+++ b/src/tests/functional/tests/bundles/focal-snap-tls.yaml
@@ -0,0 +1 @@
1base.yaml
0\ No newline at end of file2\ No newline at end of file
diff --git a/src/tests/functional/tests/bundles/focal-tls.yaml b/src/tests/functional/tests/bundles/focal-tls.yaml
1new file mode 1200003new file mode 120000
index 0000000..f81f6ff
--- /dev/null
+++ b/src/tests/functional/tests/bundles/focal-tls.yaml
@@ -0,0 +1 @@
1base.yaml
0\ No newline at end of file2\ No newline at end of file
diff --git a/src/tests/functional/tests/bundles/overlays/bionic-snap-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/bionic-snap-tls.yaml.j2
1new file mode 1006443new file mode 100644
index 0000000..e302099
--- /dev/null
+++ b/src/tests/functional/tests/bundles/overlays/bionic-snap-tls.yaml.j2
@@ -0,0 +1,11 @@
1series: bionic
2applications:
3 grafana:
4 options:
5 install_method: snap
6 snap_channel: 6/stable
7 easyrsa:
8 charm: cs:~containers/easyrsa
9 num_units: 1
10relations:
11 - [ grafana:certificates, easyrsa ]
diff --git a/src/tests/functional/tests/bundles/overlays/bionic-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/bionic-tls.yaml.j2
0new file mode 10064412new file mode 100644
index 0000000..e728037
--- /dev/null
+++ b/src/tests/functional/tests/bundles/overlays/bionic-tls.yaml.j2
@@ -0,0 +1,10 @@
1series: bionic
2applications:
3 grafana:
4 options:
5 install_method: apt
6 easyrsa:
7 charm: cs:~containers/easyrsa
8 num_units: 1
9relations:
10 - [ grafana:certificates, easyrsa ]
diff --git a/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2
0new file mode 10064411new file mode 100644
index 0000000..259f3c5
--- /dev/null
+++ b/src/tests/functional/tests/bundles/overlays/focal-snap-tls.yaml.j2
@@ -0,0 +1,32 @@
1series: focal
2applications:
3 grafana:
4 num_units: 1
5 options:
6 install_method: snap
7 snap_channel: 6/stable
8 ceph-mon:
9 series: bionic
10 ceph-osd:
11 series: bionic
12 glance:
13 series: bionic
14 keystone:
15 series: bionic
16 mysql:
17 series: bionic
18 rabbitmq-server:
19 series: bionic
20 prometheus:
21 series: bionic
22 prometheus-ceph-exporter:
23 series: bionic
24 prometheus-libvirt-exporter:
25 series: bionic
26 nagios:
27 series: bionic
28 easyrsa:
29 charm: cs:~containers/easyrsa
30 num_units: 1
31relations:
32 - [ grafana:certificates, easyrsa ]
diff --git a/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2
0new file mode 10064433new file mode 100644
index 0000000..b6283dc
--- /dev/null
+++ b/src/tests/functional/tests/bundles/overlays/focal-tls.yaml.j2
@@ -0,0 +1,31 @@
1series: focal
2applications:
3 grafana:
4 num_units: 1
5 options:
6 install_method: apt
7 ceph-mon:
8 series: bionic
9 ceph-osd:
10 series: bionic
11 glance:
12 series: bionic
13 keystone:
14 series: bionic
15 mysql:
16 series: bionic
17 rabbitmq-server:
18 series: bionic
19 prometheus:
20 series: bionic
21 prometheus-ceph-exporter:
22 series: bionic
23 prometheus-libvirt-exporter:
24 series: bionic
25 nagios:
26 series: bionic
27 easyrsa:
28 charm: cs:~containers/easyrsa
29 num_units: 1
30relations:
31 - [ grafana:certificates, easyrsa ]
diff --git a/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2 b/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2
0new file mode 10064432new file mode 100644
index 0000000..86f7ebf
--- /dev/null
+++ b/src/tests/functional/tests/bundles/overlays/xenial-tls.yaml.j2
@@ -0,0 +1,12 @@
1series: xenial
2applications:
3 grafana:
4 options:
5 install_method: apt
6 easyrsa:
7 # Use bionic version as easyrsa is failing to install on xenial
8 series: bionic
9 charm: cs:~containers/easyrsa
10 num_units: 1
11relations:
12 - [ grafana:certificates, easyrsa ]
diff --git a/src/tests/functional/tests/bundles/xenial-tls.yaml b/src/tests/functional/tests/bundles/xenial-tls.yaml
0new file mode 12000013new file mode 120000
index 0000000..f81f6ff
--- /dev/null
+++ b/src/tests/functional/tests/bundles/xenial-tls.yaml
@@ -0,0 +1 @@
1base.yaml
0\ No newline at end of file2\ No newline at end of file
diff --git a/src/tests/functional/tests/test_grafana.py b/src/tests/functional/tests/test_grafana.py
index d0e54d4..03fe575 100644
--- a/src/tests/functional/tests/test_grafana.py
+++ b/src/tests/functional/tests/test_grafana.py
@@ -1,6 +1,8 @@
1"""Encapsulate prometheus-openstack-exporter testing."""1"""Encapsulate prometheus-openstack-exporter testing."""
2import json2import json
3import logging3import logging
4import os
5import tempfile
4import time6import time
5import unittest7import unittest
68
@@ -16,7 +18,10 @@ DEFAULT_BACKUP_DIRECTORY = "/srv/backups"
1618
1719
18class BaseGrafanaTest(unittest.TestCase):20class BaseGrafanaTest(unittest.TestCase):
19 """Base for Prometheus-openstack-exporter charm tests."""21 """Base for Prometheus-openstack-exporter charm tests.
22
23 - Get the CA from easyrsa (when available) and write it on disk.
24 """
2025
21 _admin_pass = None26 _admin_pass = None
2227
@@ -32,11 +37,45 @@ class BaseGrafanaTest(unittest.TestCase):
32 cls.units = model.get_units(cls.application_name, model_name=cls.model_name)37 cls.units = model.get_units(cls.application_name, model_name=cls.model_name)
33 cls.grafana_ip = model.get_app_ips(cls.application_name)[0]38 cls.grafana_ip = model.get_app_ips(cls.application_name)[0]
3439
40 # get the CA certificate from the relation between easy easyrsa and
41 # grafana.
42 if cls.get_protocol() == "https":
43 rel_id = model.get_relation_id(
44 "grafana", "easyrsa", remote_interface_name="client"
45 )
46 easyrsa_unit = model.get_units("easyrsa")[0]
47 cmd = "relation-get -r client:{} ca {}".format(
48 rel_id, easyrsa_unit.entity_id
49 )
50 logging.info(cmd)
51 result = model.run_on_unit(easyrsa_unit.entity_id, cmd)
52
53 with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
54 f.write(result["Stdout"])
55 f.flush()
56 cls.ca_path = f.name
57 else:
58 cls.ca_path = None
59
60 @classmethod
61 def tearDownClass(cls):
62 """Remove the CA file that was written to disk during the setUp."""
63 if cls.ca_path:
64 os.remove(cls.ca_path)
65
35 def get_unit_status(self, unit):66 def get_unit_status(self, unit):
36 """Get grafana unit workload status."""67 """Get grafana unit workload status."""
37 u = model.get_unit_from_name(unit)68 u = model.get_unit_from_name(unit)
38 return u.workload_status_message69 return u.workload_status_message
3970
71 @classmethod
72 def get_protocol(cls):
73 """Get protocol configured to serve Grafana."""
74 if model.get_relation_id("grafana", "easyrsa"):
75 return "https"
76 else:
77 return "http"
78
40 @property79 @property
41 def admin_password(self):80 def admin_password(self):
42 """Get grafana admin password."""81 """Get grafana admin password."""
@@ -58,11 +97,14 @@ class BaseGrafanaTest(unittest.TestCase):
58 dashboards = []97 dashboards = []
59 while True:98 while True:
60 req = requests.get(99 req = requests.get(
61 "http://{host}:{port}"100 "{protocol}://{host}:{port}"
62 "/api/search?dashboardIds".format(101 "/api/search?dashboardIds".format(
63 host=self.grafana_ip, port=DEFAULT_API_PORT102 protocol=self.get_protocol(),
103 host=self.grafana_ip,
104 port=DEFAULT_API_PORT,
64 ),105 ),
65 auth=("admin", self.admin_password),106 auth=("admin", self.admin_password),
107 verify=self.ca_path,
66 )108 )
67 self.assertTrue(req.status_code == 200)109 self.assertTrue(req.status_code == 200)
68 dashboards = json.loads(req.text)110 dashboards = json.loads(req.text)
@@ -113,11 +155,19 @@ class CharmOperationTest(BaseGrafanaTest):
113 Curl the api endpoint.155 Curl the api endpoint.
114 We'll retry until the TEST_TIMEOUT.156 We'll retry until the TEST_TIMEOUT.
115 """157 """
158 if self.get_protocol() == "https":
159 # -S instructs the check_http plugin to check the SSL port which
160 # defaults to 443.
161 cmd = "/usr/lib/nagios/plugins/check_http -S"
162 else:
163 cmd = "/usr/lib/nagios/plugins/check_http"
164
116 test_command = "{} -I 127.0.0.1 -p {} -u {}".format(165 test_command = "{} -I 127.0.0.1 -p {} -u {}".format(
117 "/usr/lib/nagios/plugins/check_http",166 cmd,
118 DEFAULT_API_PORT,167 DEFAULT_API_PORT,
119 DEFAULT_API_URL,168 DEFAULT_API_URL,
120 )169 )
170 logging.debug("test api ready command: %s" % test_command)
121 timeout = time.time() + TEST_TIMEOUT171 timeout = time.time() + TEST_TIMEOUT
122 while time.time() < timeout:172 while time.time() < timeout:
123 response = model.run_on_unit(self.lead_unit_name, test_command)173 response = model.run_on_unit(self.lead_unit_name, test_command)
@@ -130,9 +180,11 @@ class CharmOperationTest(BaseGrafanaTest):
130180
131 # we didn't get rc=0 in the allowed time, fail the test181 # we didn't get rc=0 in the allowed time, fail the test
132 self.fail(182 self.fail(
133 "http port didn't respond to the command \n"183 "{protocol} port didn't respond to the command \n"
134 "'{test_command}' as expected.\n"184 "'{test_command}' as expected.\n"
135 "Result: {result}".format(test_command=test_command, result=response)185 "Result: {result}".format(
186 protocol=self.get_protocol(), test_command=test_command, result=response
187 )
136 )188 )
137189
138 def test_02_nrpe_http_check(self):190 def test_02_nrpe_http_check(self):
@@ -140,10 +192,19 @@ class CharmOperationTest(BaseGrafanaTest):
140 logging.debug(192 logging.debug(
141 "Verify the nrpe check is created and has the required content..."193 "Verify the nrpe check is created and has the required content..."
142 )194 )
143 expected_nrpe_check = (195 if self.get_protocol() == "https":
144 "command[check_grafana_http]=/usr/lib/nagios/plugins/check_http "196 expected_nrpe_check = (
145 "-I 127.0.0.1 -p 3000 -u /login"197 "command[check_grafana_http]="
146 )198 "/usr/lib/nagios/plugins/check_http "
199 "-S -I 127.0.0.1 -p 3000 -u /login"
200 )
201 else:
202 expected_nrpe_check = (
203 "command[check_grafana_http]="
204 "/usr/lib/nagios/plugins/check_http "
205 "-I 127.0.0.1 -p 3000 -u /login"
206 )
207
147 cmd = "cat /etc/nagios/nrpe.d/check_grafana_http.cfg"208 cmd = "cat /etc/nagios/nrpe.d/check_grafana_http.cfg"
148 result = model.run_on_unit(self.lead_unit_name, cmd)209 result = model.run_on_unit(self.lead_unit_name, cmd)
149 code = result.get("Code")210 code = result.get("Code")
@@ -213,10 +274,13 @@ class CharmOperationTest(BaseGrafanaTest):
213 self.assertTrue(action.data["results"]["Code"] == "0")274 self.assertTrue(action.data["results"]["Code"] == "0")
214 time.sleep(30) # Dirty hack to overcome race condition275 time.sleep(30) # Dirty hack to overcome race condition
215 req = requests.get(276 req = requests.get(
216 "http://{host}:{port}/api/org/".format(277 "{protocol}://{host}:{port}/api/org/".format(
217 host=self.grafana_ip, port=DEFAULT_API_PORT278 protocol=self.get_protocol(),
279 host=self.grafana_ip,
280 port=DEFAULT_API_PORT,
218 ),281 ),
219 auth=("foouser", "sikkrit"),282 auth=("foouser", "sikkrit"),
283 verify=self.ca_path,
220 )284 )
221 self.assertTrue(req.status_code == 200)285 self.assertTrue(req.status_code == 200)
222 self.assertTrue("name" in req.json())286 self.assertTrue("name" in req.json())
diff --git a/src/tests/functional/tests/tests.yaml b/src/tests/functional/tests/tests.yaml
index 909b776..736607f 100644
--- a/src/tests/functional/tests/tests.yaml
+++ b/src/tests/functional/tests/tests.yaml
@@ -1,10 +1,15 @@
1charm_name: grafana1charm_name: grafana
2gate_bundles:2gate_bundles:
3 - model_apt_install: focal3 - model_apt_install: focal
4 - model_apt_install: focal-tls
4 - model_apt_install: bionic5 - model_apt_install: bionic
6 - model_apt_install: bionic-tls
5 - model_apt_install: xenial7 - model_apt_install: xenial
8 - model_apt_install: xenial-tls
6 - model_snap_install: bionic-snap9 - model_snap_install: bionic-snap
10 - model_snap_install: bionic-snap-tls
7 - model_snap_install: focal-snap11 - model_snap_install: focal-snap
12 - model_snap_install: focal-snap-tls
8smoke_bundles:13smoke_bundles:
9 - model_snap_install: bionic-snap14 - model_snap_install: bionic-snap
10dev_bundles:15dev_bundles:
@@ -25,3 +30,5 @@ target_deploy_status:
25 workload-status-message: Monitoring30 workload-status-message: Monitoring
26 prometheus-libvirt-exporter:31 prometheus-libvirt-exporter:
27 workload-status-message: "Exporter installed and connected to libvirt slot"32 workload-status-message: "Exporter installed and connected to libvirt slot"
33 easyrsa:
34 workload-status-message: Certificate Authority connected.
diff --git a/src/tests/unit/test_grafana.py b/src/tests/unit/test_grafana.py
index 663fa17..2e460ef 100644
--- a/src/tests/unit/test_grafana.py
+++ b/src/tests/unit/test_grafana.py
@@ -1,4 +1,6 @@
1"""Unit tests module."""1"""Unit tests module."""
2import os
3import socket
2import sys4import sys
3import unittest5import unittest
4from os.path import isfile6from os.path import isfile
@@ -7,6 +9,8 @@ from unittest.mock import call
79
8from charmhelpers.core import unitdata10from charmhelpers.core import unitdata
911
12tls_client_mock = mock.Mock()
13sys.modules["charms.layer.tls_client"] = tls_client_mock
10sys.modules["charms.layer.snap"] = mock.Mock()14sys.modules["charms.layer.snap"] = mock.Mock()
1115
1216
@@ -148,3 +152,47 @@ class GrafanaTestCase(unittest.TestCase):
148 "dash-name",152 "dash-name",
149 {"dashboard": {"title": "[juju-foo-app] test-title"}, "folderId": 0},153 {"dashboard": {"title": "[juju-foo-app] test-title"}, "folderId": 0},
150 )154 )
155
156 @mock.patch("charmhelpers.core.hookenv.charm_dir", auto_spec=True)
157 @mock.patch("reactive.grafana.templating.render", auto_spec=True)
158 @mock.patch("reactive.grafana.hookenv.config", auto_spec=True)
159 @mock.patch("reactive.grafana.hookenv.unit_public_ip", auto_spec=True)
160 @mock.patch("reactive.grafana.hookenv.unit_private_ip", auto_spec=True)
161 @mock.patch("subprocess.check_call", auto_spec=True)
162 def test_request_certificate(
163 self,
164 mock_check_call,
165 mock_private_ip,
166 mock_public_ip,
167 mock_config,
168 mock_render,
169 mock_charm_dir,
170 ):
171 """Test request certificate."""
172 charm_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
173 mock_charm_dir.return_value = charm_dir
174
175 config = {"install_method": "snap"}
176
177 def fake_config(key):
178 return config[key]
179
180 mock_config.side_effect = fake_config
181 mock_public_ip.return_value = "1.2.3.4"
182 mock_private_ip.return_value = "5.6.7.8"
183
184 mock_tls = mock.Mock()
185 grafana_reactive.tls_request_certificate(mock_tls)
186 tls_client_mock.request_server_cert.assert_called_with(
187 "1.2.3.4",
188 ["1.2.3.4", "5.6.7.8", socket.gethostname(), "127.0.0.1", "localhost"],
189 crt_path=grafana_reactive.CERT_PATH["snap"],
190 key_path=grafana_reactive.CERT_KEY_PATH["snap"],
191 )
192
193 mock_render.assert_called_with(
194 "sync-grafana-snap",
195 grafana_reactive.CA_CERTIFICATES_HOOK,
196 context={"CA_CERT_PATH": grafana_reactive.CA_CERT_PATH},
197 perms=0o755,
198 )
diff --git a/src/tox.ini b/src/tox.ini
index 5845e03..72d33e7 100644
--- a/src/tox.ini
+++ b/src/tox.ini
@@ -21,6 +21,7 @@ passenv =
21 NO_PROXY21 NO_PROXY
22 SNAP_HTTP_PROXY22 SNAP_HTTP_PROXY
23 SNAP_HTTPS_PROXY23 SNAP_HTTPS_PROXY
24<<<<<<< src/tox.ini
24 OS_REGION_NAME25 OS_REGION_NAME
25 OS_AUTH_VERSION26 OS_AUTH_VERSION
26 OS_AUTH_URL27 OS_AUTH_URL
@@ -31,6 +32,9 @@ passenv =
31 OS_USER_DOMAIN_NAME32 OS_USER_DOMAIN_NAME
32 OS_PROJECT_NAME33 OS_PROJECT_NAME
33 OS_IDENTITY_API_VERSION34 OS_IDENTITY_API_VERSION
35=======
36 OS_*
37>>>>>>> src/tox.ini
3438
35[testenv:lint]39[testenv:lint]
36commands =40commands =

Subscribers

People subscribed via source and target branches

to all changes: