Merge ~graylog-charmers/charm-graylog:feature/multi-version into ~graylog-charmers/charm-graylog:master

Proposed by Kevin W Monroe
Status: Merged
Approved by: Kevin W Monroe
Approved revision: 1b0b6d2105f26a1df236f7928e77353e54b260a1
Merged at revision: 7cb4b82ab266da4e755142dbff3b5836b537bc38
Proposed branch: ~graylog-charmers/charm-graylog:feature/multi-version
Merge into: ~graylog-charmers/charm-graylog:master
Diff against target: 1031 lines (+502/-145)
11 files modified
README.md (+61/-25)
config.yaml (+32/-11)
layer.yaml (+5/-2)
lib/charms/layer/graylog/__init__.py (+3/-0)
lib/charms/layer/graylog/logextract.py (+8/-3)
lib/charms/layer/graylog/utils.py (+42/-0)
reactive/graylog.py (+152/-91)
unit_tests/test_graylog.py (+155/-6)
unit_tests/test_lib.py (+36/-0)
unit_tests/test_logextract.py (+7/-7)
wheelhouse.txt (+1/-0)
Reviewer Review Type Date Requested Status
Kevin W Monroe Approve
Paul Goins Approve
Stuart Bishop (community) Approve
Edward Hope-Morley Pending
Canonical IS Reviewers Pending
Review via email: mp+371274@code.launchpad.net

Commit message

Enable snap 'channel' config so users can switch between graylog versions.

Description of the change

This enables the charm operator to configure the major graylog version used when installing graylog from the snap store.

This is still a WIP as the upgrade from 2->3 hasn't been fully tested. It is, however, ready for early feedback. Folks that want to kick the tires can deploy ~graylog-charmers/graylog/43:

https://jaas.ai/u/graylog-charmers/graylog/43

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
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Unable to determine commit message from repository - please click "Set commit message" and enter the commit message manually.

Revision history for this message
Stuart Bishop (stub) wrote :

Looks good. Some minor comments inline, which you might want to change before this lands.

web_listen_uri, web_endpoint_uri, rest_transport_uri... oh my. I don't think this stuff should be in the config.yaml, instead the charm setting good defaults using the Juju network information. But they already exist, so reasonable that this is working with what we have rather than proposing a major backwards incompatible change :)

review: Approve
Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

@stub, I agree that a lot of this charm config is a mess. Perhaps when we eventually make v3 the default, we can work on smarter bindings and deprecate at least a handful of these opts.

Thanks for the is_v2 helper idea! It simplified both reactive and lib code nicely. I implemented the channel handler so snap refresh/reconfig work like they should. And what the heck, i threw in some unit tests.

One important thing to note: I've made graylog.snap a 0-byte resource for the edge and beta channels. I felt it better for the default behavior to be a snap store install using a configured channel. The alternative would be to continue hosting the 150MB graylog.snap (v2), then requiring users to pull down and attach a 150MB v3 snap to get v3 deployed.

If this change in default behavior irks anyone, please chime in. Latest changes can be found in rev 44, currently in beta:

https://jaas.ai/u/graylog-charmers/graylog/44

Revision history for this message
Paul Goins (vultaire) wrote :

Overall, looks pretty nice.

I've provided a few comments for review. Note that only some of these "require" changes I think, but please look at all 4.

Additionally... In Graylog 3, a few APIs also changed, and I don't see those reflected in this change set. Specifically, I'm referring to these: https://git.launchpad.net/~vultaire/graylog-charm/diff/lib/charms/layer/graylog/logextract.py?h=graylog3&id=0b6162815e80e3b8ba73542b2d9dd326f74fa307

(Note the above is from a quick-and-dirty, backwards-incompatible fork of the Graylog charm I wrote for testing Graylog 3; it cannot be used as is since we need to call the correct API based on the graylog version.)

review: Needs Fixing
Revision history for this message
Paul Goins (vultaire) wrote :

Withdrew one of my comments.

Revision history for this message
Rodrigo Barbieri (rodrigo-barbieri2010) wrote :

Updated with the review feedback of Paul Groins.

Changed

@when_not('snap.installed.graylog')
def install_graylog():

to

@when('snap.installed.core')
def install_graylog():

Revision history for this message
Paul Goins (vultaire) wrote :

Re: my previous comments regarding v2/v3 APIs: as I don't think those bits are actually being used (since the config key that exposes them isn't exposed via config.yaml yet), they can be fixed later.

My only remaining concern remains regarding the description of the web_listen_uri config value; see my previously-posted comment on config.yaml from 2019-09-03.

review: Needs Fixing
Revision history for this message
Rodrigo Barbieri (rodrigo-barbieri2010) wrote :

Thanks again Paul. I updated the config option description.

Revision history for this message
Paul Goins (vultaire) :
review: Approve
Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

@when('snap.installed.core')
def install_graylog():

Can't do that ^^. Reactive will run 'install_graylog' every 5 minutes (every update_status hook) if we do.

review: Needs Fixing
Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

On Wed, Sep 4, 2019 at 1:58 AM Paul Goins <email address hidden> wrote:

> > diff --git a/lib/charms/layer/graylog/__init__.py
> b/lib/charms/layer/graylog/__init__.py
> > index e69de29..38bffbe 100644
> > --- a/lib/charms/layer/graylog/__init__.py
> > +++ b/lib/charms/layer/graylog/__init__.py
> > @@ -0,0 +1,39 @@
> > +from charmhelpers.core.hookenv import log
>
> Purely stylistic; don't consider this a -1. (At least not from me, as I'm
> somewhat new to reactive charms.)
>
> Per charms.reactive docs, this type of layer lib file seems intended for
> base layers rather than charm layers. And I do see similar helper
> functions directly in the reactive/graylog.py module.
>
> Is the intent for this to be reused by other charms? Or simply as a "lib"
> module separate from the handlers?
>

The latter; this module is meant to be helpful funcs for graylog that
aren't reactive. The helpers you see in reactive/gl.py should also be in
this lib. Ideally, only @reactive funcs would appear in reactive/*.py, but
we didn't start that way, so that's not how it is today. That said, we
don't have to keep going down the wrong road. There's an opportunity for
better organization in this charm, but that's beyond the scope of the v3
changes proposed here.

> diff --git a/reactive/graylog.py b/reactive/graylog.py
> > index 0d64f26..4a718bd 100644
> > --- a/reactive/graylog.py
> > +++ b/reactive/graylog.py
> > @@ -55,15 +67,56 @@ rotation_strategies = {
> > }
> >
> >
> > +@when_not('snap.installed.graylog')
>
> I've got a little concern here. If I understand the charms.reactive docs
> correctly; it doesn't seem that there's a guaranteed order that multiple
> matching handlers get executed in. (I'm relatively new to reactive charms
> so I may be wrong; correct me if so!)
>
> I think there's no guarantee here whether the snap layer's install()
> function or this install_graylog() function will be run first.
>
> While it feels kind of dirty, I think one way to guarantee this runs after
> the snap layer w/o requiring changes to it would be to add a
> @when('snap.installed.core') decorator, as the snap layer will set that
> once it runs its install() function. (I'd prefer a clearer flag that the
> snap install handler has run, but this should work I think.)
>

The problem with @when('snap.installed.core') is that once it's set, it's
set forever. That means every hook will trigger that handler until the end
of time. Yes, it's not a big handler -- we'd call "snap install graylog"
on the system and it would come back with "hey, it's already installed".
Still, it's burning cycles needlessly. I understand your concern, but snap
is smart enough that we don't actually have to wait for 'core'. If you do
a 'snap install hello-world' and 'core' isn't there, snapd will bring it
(and any other prereqs) down first.

It's safe to do @when_not(snap.installed.graylog) to guard the install
regardless of prereqs like 'core' being installed. In fact, layer-snap's
install function will set 'snap.installed.core' if it's not already:

https://git.launchpad.net/layer-snap/tree/lib/charms/layer/snap.py#n60

Revision history for this message
Paul Goins (vultaire) wrote :

Hi Kevin,

Thanks for the explanation - you corrected some misunderstandings I had about reactive charm flow. I was treating everything similar to how you'd treat e.g. "config.changed", i.e. only set when something has changed.

Revision history for this message
Paul Goins (vultaire) wrote :

The above being said, the intent of my review comment wasn't to remove "@when_not('snap.installed.graylog')", but merely to add "@when('snap.installed.core')", e.g.:

  @when('snap.installed.core')
  @when_not('snap.installed.graylog')
  def install_graylog():
      [....]

However, I did approve the change where the @when_not decorator was removed, which was a miss.

I think if both decorators are there, it'll run only once *and* only when Graylog isn't installed via the snap layer.

What do you think?

Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

I addressed previous comments in this last round of commits. Specifically:

- Guard install_graylog with @when_not(snap.installed.graylog). While I appreciate the idea of @when(core); @when_not(graylog), it's not needed for a couple reasons: (1) snap.install will ensure all prereqs are installed. (2) if 'core' ever changed names or graylog grew a new prereq, we'd have to come back and update the install_graylog decorators. Let's let snap handle the install just like we would apt.

- The beats input was being reconfigured every hook. There's no need for that. Instead, only reconfigure when config changes.

- From an earlier __init__.py comment, I decided to refactor the lib/charms/layer/graylog package. Move utils to a proper utils.py and make the relevant submodule bits known in __init__.py.

- From an earlier logextract.py comment, set the right URL depending on which version of graylog is installed. (Big Thanks for this heads up -- i didn't realize the URLs had changed)

- Test and Readme updates.

Interested parties can kick the tires with cs:~graylog-charmers/graylog-45, which has been released through candidate.

review: Approve
Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

Latest round of updates addresses how we acknowledge the snap channel when upgrading from a charm that doesn't include that option. Also leverages leader unit data so we don't configure graylog until leader passwords are setup (we need all units to agree on the passwords).

And finally, i noticed the nagios handler wouldn't change the api url/port on upgrade. This has been fixed.

Pushed through candidate in cs:~graylog-charmers/graylog-49

Revision history for this message
Kevin W Monroe (kwmonroe) wrote :

IS deploy/upgrade tests were successful:

https://jenkins.canonical.com/is-mojo-ci/job/live-is-charm-test-graylog-bionic/113/

I also ran this through a round of manual charmed k8s integration tests which passed (graylog v3 showed extended k8s metadata in the logs).

Thanks to all for the reviews/suggestions. Rev cs:~graylog-charmers/graylog-49 is now in stable.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change has no commit message, setting status to needs review.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 7cb4b82ab266da4e755142dbff3b5836b537bc38

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index a8174c7..94a4b69 100644
3--- a/README.md
4+++ b/README.md
5@@ -1,65 +1,101 @@
6 # Overview
7
8-The charm installs [Graylog](https://www.graylog.org/) using the [snap package](https://uappexplorer.com/snap/ubuntu/graylog).
9-The charm must be related to elasticsearch and mongodb in order to be a fully functioning installation.
10+The charm installs [Graylog](https://www.graylog.org/) using the [snap package](https://snapcraft.io/graylog).
11
12-# Usage
13+## Usage
14
15+```bash
16 juju deploy cs:~graylog-charmers/graylog
17-juju run-action graylog/X show-admin-password
18-juju show-action-output <action ID>
19+juju run-action --wait graylog/X show-admin-password
20+```
21
22 Graylog requires MongoDB to run and Elasticsearch to be useful.
23
24-juju deploy cs:mongodb
25+```bash
26+juju deploy cs:~mongodb-charmers/mongodb
27 juju relate graylog:mongodb mongodb:database
28+
29 juju deploy cs:~elasticsearch-charmers/elasticsearch
30 juju relate graylog:elasticsearch elasticsearch:client
31+```
32
33-You can then browse to http://ip-address:9000 and log in as the user "admin".
34-The password is by default a random value so 'juju run-action --wait graylog/X show-admin-password' must be run for admin access to the installation.
35+You can then browse to `http://ip-address:9000` and log in as the user "admin".
36+The password is by default a random value so `juju run-action --wait graylog/X show-admin-password` must be run for
37+admin access to the installation.
38
39 ## Reverseproxy Relation
40-Graylog supports advertising its web and api ports to an application acting as a reverseproxy using the http relation.
41-The port of the webUI is exposed over the relation as is the port for both the webUI and API in the all_services variable of the relation.
42-More details on using this are in the reverseproxy instructions for the [Apache2 charm](https://jujucharms.com/apache2/).
43
44-For example, you could use the following as a graylog vhost template for the apache2 charm.
45-Note that you'll need to update the GRAYLOG_UNIT_IP in the template below to match the IP of your graylog/X unit.
46+Graylog supports advertising its ports to an application acting as a reverseproxy using the `http` relation. The port
47+of the webUI is exposed over the relation in the `all_services` variable of the relation.
48
49-```
50+>**Note**: For Graylog version 2, the API port is also exposed over the `http` relation. Graylog version 3 hard codes
51+the `/api/` location and uses the default port (9000) for both webUI and API.
52+
53+More details on using this are in the reverseproxy instructions for the
54+[Apache2 charm](https://jujucharms.com/apache2/).
55+
56+Sample Graylog 2 vhost template for the apache2 charm:
57+
58+```bash
59 $ cat graylog-vhost.tmpl
60 <Location "/">
61 RequestHeader set X-Graylog-Server-URL "http://{{servername}}/api/"
62- ProxyPass http://GRAYLOG_UNIT_IP:9000/
63- ProxyPassReverse http://GRAYLOG_UNIT_IP:9000/
64+ ProxyPass http://{{graylog_web}}/
65+ ProxyPassReverse http://{{graylog_web}}/
66 </Location>
67
68 <Location "/api/">
69- ProxyPass http://GRAYLOG_UNIT_IP:9001/api/
70- ProxyPassReverse http://GRAYLOG_UNIT_IP:9001/api/
71+ ProxyPass http://{{graylog_api}}/api/
72+ ProxyPassReverse http://{{graylog_api}}/api/
73 </Location>
74 ```
75
76-Now deploy and configure apache2 as your graylog reverse proxy:
77+Sample Graylog 3 vhost template for the apache2 charm:
78
79+```bash
80+$ cat graylog-vhost.tmpl
81+<Location "/">
82+ RequestHeader set X-Graylog-Server-URL "http://{{servername}}/"
83+ ProxyPass http://{{graylog_web}}/
84+ ProxyPassReverse http://{{graylog_web}}/
85+</Location>
86 ```
87+
88+Now deploy and configure apache2 as your graylog reverse proxy:
89+
90+```bash
91 juju deploy apache2
92 juju config apache2 "enable_modules='headers proxy_html proxy_http'"
93-juju config apache2 "vhost_http_template=$(base64 < graylog-vhost.tmpl)"
94+juju config apache2 "vhost_http_template=$(base64 ./graylog-vhost.tmpl)"
95 juju expose apache2
96 juju relate apache2:reverseproxy graylog:website
97 ```
98
99-Visit http://[apache2-public-ip] to access the Graylog interface.
100+Visit `http://<apache2-public-ip>` to access the Graylog web interface.
101
102 ## Scale out Usage
103
104-The MongoDB and Elasticsearch applications can both be scaled to clusters and Graylog will adapt to using the cluster.
105+The MongoDB and Elasticsearch applications can both be scaled up or down. Graylog will reconfigure itself as needed.
106 The Graylog charm does not yet support clustering of multiple units.
107
108-# Configuration
109+## Configuration
110+
111+Depending on the Elasticsearch charm used, the cluster name may not be passed to Graylog. In this case, the
112+`elasticsearch_cluster_name` config option should be set.
113+
114+## Upgrade
115+
116+Graylog may be upgraded to a different snap version by setting the `channel` config option. For example, switch to the
117+latest version 3 edge snap with the following:
118+
119+```bash
120+juju config graylog channel='3/edge'
121+```
122
123-The administrator password is by default a random value so 'juju run-action --wait graylog/X show-admin-password' must be run for admin access to the installation.
124+>**Note**: When upgrading from Graylog version 2 to version 3, please consult the
125+[upgrade guide](https://docs.graylog.org/en/3.1/pages/upgrade.html) to ensure your environment meets the minimum
126+requirements.
127
128-Depending on the Elasticsearch charm used the cluster name may not be passed in the relation in which case it the `elasticsearch_cluster_name` config option should be set.
129+If a new `channel` config option results in a new snap being installed, the charm will backup the previous
130+configuration file on the graylog unit in `/var/snap/graylog/common/server.conf.$prev`. This may be useful if
131+graylog needs to be reverted to a previous version in the future.
132diff --git a/config.yaml b/config.yaml
133index 77bc734..d0d2649 100644
134--- a/config.yaml
135+++ b/config.yaml
136@@ -1,4 +1,11 @@
137 options:
138+ channel:
139+ type: string
140+ default: "2/stable"
141+ description: |
142+ Snap channel used to install/refresh the graylog snap.
143+
144+ This option has no effect when a valid graylog.snap resource is attached.
145 elasticsearch_cluster_name:
146 type: string
147 default: ""
148@@ -6,24 +13,38 @@ options:
149 web_listen_uri:
150 type: string
151 default: "http://0.0.0.0:9000/"
152- description: The uri the web interface will be available at.
153+ description: |
154+ The uri the web interface will be available at. In version 3 and higher,
155+ this is used for and converted to the appropriate format for
156+ the 'http_bind_address' config value.
157 web_endpoint_uri:
158 type: string
159 default: ""
160- description: >
161- If set, this will be published as the external address for connecting REST
162- API of the Graylog server. Web interface clients need to be able to
163- connect to this for the web interface to work.
164- By default, will use `rest_transport_uri` if defined, or will select one
165- of the node's allocated ips on port 9001. Example: http://10.0.0.1:9001/
166+ description: |
167+ If set, this will be published as the external address for connecting to
168+ the REST API of the Graylog server. Web interface clients need to be able
169+ to connect to this for the web interface to work.
170+
171+ In version 2, Graylog will set the default value to 'rest_transport_uri'
172+ if defined; otherwise it will select the first non-loopback IPv4 address
173+ on port 9001. Example: http://10.0.0.1:9001/
174+
175+ In version 3, Graylog refers to this option as 'http_external_uri' with
176+ the default value being 'rest_transport_uri' if defined; otherwise it will
177+ select the first non-loopback IPv4 address on port 9000.
178+ Example: http://10.0.0.1:9000/
179 rest_transport_uri:
180 type: string
181 default: ""
182- description: >
183+ description: |
184 If set, this will be promoted in the cluster discovery APIs. You will need
185- to define this, if your Graylog server is running behind a HTTP proxy that
186- is rewriting the scheme, host name or URI. This must not contain a
187- wildcard address (0.0.0.0). Example: http://192.168.1.1:9001/
188+ to define this if your Graylog server is running behind a HTTP proxy that
189+ is rewriting the scheme, host name, or URI. This must not contain a
190+ wildcard address (0.0.0.0).
191+
192+ For Graylog 2, this usually takes the form http://10.0.0.1:9001/api/.
193+ For Graylog 3 and higher, this is known as the 'http_publish_uri' and looks
194+ like http://10.0.0.1:9000/.
195 index_replicas:
196 type: int
197 default: 0
198diff --git a/layer.yaml b/layer.yaml
199index d4fa469..414c56e 100644
200--- a/layer.yaml
201+++ b/layer.yaml
202@@ -8,6 +8,11 @@ includes:
203 - 'interface:http'
204 - 'interface:mongodb-cluster'
205 - 'interface:nrpe-external-master'
206+exclude:
207+ - .coverage
208+ - .tox
209+ - .unit-state.db
210+ - __pycache__
211 options:
212 basic:
213 packages:
214@@ -15,5 +20,3 @@ options:
215 snap:
216 core:
217 channel: stable
218- graylog:
219- channel: stable
220diff --git a/lib/charms/layer/graylog/__init__.py b/lib/charms/layer/graylog/__init__.py
221index e69de29..5399010 100644
222--- a/lib/charms/layer/graylog/__init__.py
223+++ b/lib/charms/layer/graylog/__init__.py
224@@ -0,0 +1,3 @@
225+from .api import GraylogApi # NOQA
226+from .logextract import GraylogPipelines, GraylogRules, GraylogStreams, LogExtractPipeline # NOQA
227+from .utils import * # NOQA
228diff --git a/lib/charms/layer/graylog/logextract.py b/lib/charms/layer/graylog/logextract.py
229index 2447da4..5d1ae27 100644
230--- a/lib/charms/layer/graylog/logextract.py
231+++ b/lib/charms/layer/graylog/logextract.py
232@@ -3,6 +3,7 @@ extraction
233 """
234
235 import re
236+from .utils import is_v2
237
238
239 class GraylogTitleSourceItems:
240@@ -47,11 +48,13 @@ class GraylogTitleSourceItems:
241
242
243 class GraylogPipelines(GraylogTitleSourceItems):
244- url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline'
245+ url = '{}/system/pipelines/pipeline'.format(
246+ '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
247
248
249 class GraylogRules(GraylogTitleSourceItems):
250- url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/rule'
251+ url = '{}/system/pipelines/rule'.format(
252+ '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
253
254
255 class GraylogStreams:
256@@ -78,7 +81,9 @@ class GraylogStreams:
257 :param pipeline title
258 :param stream title
259 """
260- url = "/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/connections/to_stream"
261+ url = '{}/system/pipelines/connections/to_stream'.format(
262+ '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
263+
264 streamid = self.get_stream_id_for(stream)
265 pipes = GraylogPipelines(self.graylogapi)
266 pipeline = pipes.get_for_title(pipeline)
267diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py
268new file mode 100644
269index 0000000..cf89b61
270--- /dev/null
271+++ b/lib/charms/layer/graylog/utils.py
272@@ -0,0 +1,42 @@
273+from charmhelpers.core import hookenv
274+from charms.layer import snap
275+
276+SNAP_NAME = 'graylog'
277+
278+
279+def get_api_port():
280+ return '9001' if is_v2() else '9000'
281+
282+
283+def get_api_url():
284+ return 'http://127.0.0.1:{}/api/'.format(get_api_port())
285+
286+
287+def is_v2():
288+ version = snap.get_installed_version(SNAP_NAME)
289+ # If the snap isn't installed yet, base our version off the charm config
290+ if not version:
291+ version = hookenv.config('channel')
292+ return True if version.startswith('2') else False
293+
294+
295+def validate_api_uri(uri):
296+ """Ensure the given API URI is valid based on the Graylog version.
297+
298+ For v2, api-related options should end with '/api/'. In v3, the graylog
299+ server hard codes '/api/' to the end of these options and should therefore
300+ be removed if included in the charm config.
301+ """
302+ if not uri.endswith('/'):
303+ uri += '/'
304+
305+ if is_v2():
306+ if not uri.endswith('/api/'):
307+ hookenv.log("Appending 'api/' to the configured REST API options")
308+ uri += 'api/'
309+ else:
310+ if uri.endswith('/api/'):
311+ hookenv.log("Removing 'api/' from the configurted REST API options")
312+ uri = uri[:-4]
313+
314+ return uri
315diff --git a/reactive/graylog.py b/reactive/graylog.py
316index b07e376..17427b4 100644
317--- a/reactive/graylog.py
318+++ b/reactive/graylog.py
319@@ -1,23 +1,31 @@
320 import hashlib
321 import os
322 import re
323+import shutil
324 import time
325 import yaml
326-from subprocess import check_output, CalledProcessError
327 from urllib.parse import urlparse
328
329-from charms.reactive import hook, when, when_not, is_state, remove_state, set_state
330+from charms.reactive import hook, when, when_any, when_not, when_not_all, is_state, remove_state, set_state
331 from charms.reactive.helpers import data_changed
332 from charmhelpers.core import host, hookenv, unitdata
333 from charmhelpers.contrib.charmsupport import nrpe
334 import charms.leadership
335
336-from charms.layer.graylog import logextract
337-from charms.layer.graylog.api import GraylogApi
338+from charms.layer import snap
339+from charms.layer.graylog import (
340+ GraylogApi,
341+ LogExtractPipeline,
342+ SNAP_NAME,
343+ get_api_port,
344+ get_api_url,
345+ is_v2,
346+ validate_api_uri
347+)
348+
349
350-API_PORT = '9001' # This is the default set by the snap
351-API_URL = 'http://127.0.0.1:9001/api/' # This is the default set by the snap
352 CONF_FILE = '/var/snap/graylog/common/server.conf'
353+SNAP_CONF_FILE = '/snap/graylog/current/etc/graylog/server/server.conf'
354 # /snap/graylog is a read-only squashfs so we use the initial settings
355 # and write out an override to SERVER_DEFAULT_CONF_FILE
356 SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server'
357@@ -32,7 +40,13 @@ def upgrade_charm():
358
359
360 @when('config.changed')
361+@when_not('config.changed.channel')
362 def update_config():
363+ """Allow new runtime config options to be processed.
364+
365+ Reset flags to allow new runtime config options to be written. Deployment
366+ config options (e.g. 'channel') are handled separately.
367+ """
368 remove_state('graylog.configured')
369
370
371@@ -55,63 +69,105 @@ rotation_strategies = {
372 }
373
374
375+@when_not('snap.installed.graylog')
376+def install_graylog():
377+ """Install the graylog snap.
378+
379+ The snap layer will first try to install a valid 'graylog.snap' resource.
380+ If a valid resource is not found, the snap will be installed from the snap
381+ store according to the configured 'channel' option.
382+ """
383+ channel = hookenv.config('channel')
384+
385+ hookenv.status_set('maintenance', 'Installing graylog snap')
386+ snap.install(SNAP_NAME, channel=channel)
387+
388+
389+@when('snap.installed.graylog')
390+@when('config.changed.channel')
391+def refresh_graylog():
392+ """Refresh the graylog snap.
393+
394+ A change to the 'channel' config option may warrant a snap refresh. If so,
395+ stop the service, backup the current config, and refresh.
396+ """
397+ channel = hookenv.config('channel')
398+ cur_snap = new_snap = snap.get_installed_version(SNAP_NAME)
399+
400+ if channel:
401+ host.service_stop(SERVICE_NAME)
402+ # backup config file using the current version as a suffix
403+ if os.path.exists(CONF_FILE):
404+ shutil.copy2(CONF_FILE, '{path}.{ext}'.format(path=CONF_FILE, ext=cur_snap))
405+
406+ hookenv.status_set('maintenance', 'Refreshing graylog snap {}'.format(channel))
407+ snap.refresh(SNAP_NAME, channel=channel)
408+ new_snap = snap.get_installed_version(SNAP_NAME)
409+ else:
410+ hookenv.log("Cannot refresh snap when the 'channel' config option is missing")
411+
412+ # When the snap version changes, use the config file from the snap and
413+ # adjust states to ensure appropriate re-config happens.
414+ if cur_snap != new_snap:
415+ shutil.copy2(SNAP_CONF_FILE, CONF_FILE)
416+ data_changed('elasticsearch.relation', None)
417+ data_changed('mongodb.uri', None)
418+ remove_state('graylog.configured')
419+
420+ report_status()
421+
422+
423 @when('snap.installed.graylog') # noqa: C901
424 @when_not('graylog.configured')
425 def configure_graylog():
426 db = unitdata.kv()
427 if not os.path.exists(CONF_FILE):
428- hookenv.log('Configuration file "{}" missing, skipping configuration run.'.format(CONF_FILE))
429- hookenv.status_set('waiting', 'Waiting for snap config: {}'.format(CONF_FILE))
430+ hookenv.status_set('maintenance', 'Waiting for {}'.format(CONF_FILE))
431 return
432 conf = hookenv.config()
433+
434 admin_password = db.get('admin_password')
435- if not admin_password:
436- admin_password = host.pwgen(18)
437- db.set('admin_password', admin_password)
438+ password_secret = db.get('password_secret')
439+ if not (admin_password and password_secret):
440+ hookenv.status_set('maintenance', 'Waiting for leader passwords')
441+ return
442 pw_hash = hashlib.sha256(admin_password.encode('utf-8')).hexdigest()
443 set_conf('root_password_sha2', pw_hash)
444+ set_conf('password_secret', password_secret)
445
446- if conf['web_listen_uri']:
447- url = urlparse(conf['web_listen_uri'])
448- webport = db.get('web_listen_port')
449+ api_port = get_api_port()
450+ if is_v2():
451+ if conf['web_listen_uri']:
452+ url = urlparse(conf['web_listen_uri'])
453+ webport = db.get('web_listen_port')
454
455- if webport == API_PORT:
456- msg = "Config 'web_listen_uri' is using port {} which conflicts with default REST port {}".format(
457- webport, API_PORT
458- )
459- hookenv.status_set('blocked', msg)
460- return
461+ if webport == api_port:
462+ msg = "Config 'web_listen_uri' is using port {} which conflicts with default REST port {}".format(
463+ webport, api_port
464+ )
465+ hookenv.status_set('blocked', msg)
466+ return
467+
468+ set_conf('web_listen_uri', conf['web_listen_uri'])
469+ if url.port != webport:
470+ if webport:
471+ hookenv.close_port(webport)
472+ db.set('web_listen_port', url.port)
473+ hookenv.open_port(url.port)
474
475- set_conf('web_listen_uri', conf['web_listen_uri'])
476- if url.port != webport:
477- if webport:
478- hookenv.close_port(webport)
479- db.set('web_listen_port', url.port)
480- hookenv.open_port(url.port)
481-
482- if conf['rest_transport_uri']:
483- set_conf('rest_transport_uri', _validate_api_uri(conf['rest_transport_uri']))
484-
485- if conf['web_endpoint_uri']:
486- set_conf('web_endpoint_uri', _validate_api_uri(conf['web_endpoint_uri']))
487-
488- hookenv.open_port(API_PORT)
489-
490- # Set application version
491- snap_name = "graylog"
492- snap_version = ""
493- cmd = ['snap', 'list', snap_name]
494- try:
495- out = check_output(cmd).decode('UTF-8')
496- except CalledProcessError:
497- pass
498+ if conf['rest_transport_uri']:
499+ set_conf('rest_transport_uri', validate_api_uri(conf['rest_transport_uri']))
500+
501+ if conf['web_endpoint_uri']:
502+ set_conf('web_endpoint_uri', validate_api_uri(conf['web_endpoint_uri']))
503 else:
504- lines = out.split('\n')
505- for line in lines:
506- if snap_name in line:
507- # Second item in list is Version
508- snap_version = line.split()[1]
509- hookenv.application_version_set(snap_version)
510+ if conf['web_listen_uri']:
511+ url = urlparse(conf['web_listen_uri'])
512+ set_conf('http_bind_address', url.netloc)
513+ if conf['rest_transport_uri']:
514+ set_conf('http_publish_uri', validate_api_uri(conf['rest_transport_uri']))
515+ if conf['web_endpoint_uri']:
516+ set_conf('http_external_uri', validate_api_uri(conf['rest_transport_uri']))
517
518 if conf.get('logextraction-rules'):
519 rules_config = yaml.safe_load(conf.get('logextraction-rules'))
520@@ -120,31 +176,32 @@ def configure_graylog():
521
522 set_jvm_heap_size(conf['jvm_heap_size'])
523
524+ hookenv.application_version_set(snap.get_installed_version(SNAP_NAME))
525+ hookenv.open_port(api_port)
526+
527 remove_state('graylog_api.configured')
528 set_state('graylog.configured')
529 report_status()
530
531
532-def _validate_api_uri(uri):
533- if not uri.endswith('/'):
534- uri += '/'
535- if not uri.endswith('/api/'):
536- uri += 'api/'
537- return uri
538-
539-
540 @when('leadership.is_leader')
541-@when_not('leadership.set.admin_password')
542-def generate_admin_password():
543- admin_password = host.pwgen(18)
544- charms.leadership.leader_set(admin_password=admin_password)
545+@when_not_all('leadership.set.admin_password', 'leadership.set.password_secret')
546+def generate_leader_passwords():
547+ if not is_state('leadership.set.admin_password'):
548+ admin_password = host.pwgen(18)
549+ charms.leadership.leader_set(admin_password=admin_password)
550+ if not is_state('leadership.set.password_secret'):
551+ password_secret = host.pwgen(96)
552+ charms.leadership.leader_set(password_secret=password_secret)
553
554
555-@when('leadership.changed.admin_password')
556-def get_leader_admin_password():
557+@when_any('leadership.changed.admin_password', 'leadership.changed.password_secret')
558+def get_leader_passwords():
559 db = unitdata.kv()
560 admin_password = charms.leadership.leader_get('admin_password')
561 db.set('admin_password', admin_password)
562+ password_secret = charms.leadership.leader_get('password_secret')
563+ db.set('password_secret', password_secret)
564 remove_state('graylog.configured')
565
566
567@@ -226,17 +283,21 @@ def report_status():
568 @when_not('graylog.needs_restart')
569 @when_not('graylog_api.configured')
570 def configure_graylog_api(*discard):
571+ """Adjust states to ensure API dependents will be correctly configured."""
572+ remove_state('beat.setup')
573 remove_state('graylog_index_sets.configured')
574 remove_state('graylog_inputs.configured')
575+ remove_state('graylog_nagios.configured')
576 set_state('graylog_api.configured')
577
578
579-@when('graylog_api.configured') # noqa: C901
580+@when('graylog.configured') # noqa: C901
581+@when('graylog_api.configured')
582 @when_not('graylog_index_sets.configured')
583 def configure_index_sets(*discard):
584 conf = hookenv.config()
585 db = unitdata.kv()
586- g = GraylogApi(base_url=API_URL,
587+ g = GraylogApi(base_url=get_api_url(),
588 username='admin',
589 password=db.get('admin_password'),
590 token_name='graylog-charm')
591@@ -289,11 +350,11 @@ def configure_index_sets(*discard):
592
593 def _configure_logextraction(rules_config):
594 db = unitdata.kv()
595- g = GraylogApi(base_url=API_URL,
596+ g = GraylogApi(base_url=get_api_url(),
597 username='admin',
598 password=db.get('admin_password'),
599 token_name='graylog-charm')
600- logext = logextract.LogExtractPipeline(g)
601+ logext = LogExtractPipeline(g)
602 logext.set_rules(rules_config)
603
604
605@@ -344,7 +405,7 @@ def _remove_old_inputs(inputs, new_inputs):
606 UI.
607 """
608 db = unitdata.kv()
609- g = GraylogApi(base_url=API_URL,
610+ g = GraylogApi(base_url=get_api_url(),
611 username='admin',
612 password=db.get('admin_password'),
613 token_name='graylog-charm')
614@@ -373,8 +434,7 @@ def graylog_remove_filebeat_input():
615 hookenv.log("Removing filebeats input")
616
617 db = unitdata.kv()
618-
619- g = GraylogApi(base_url=API_URL,
620+ g = GraylogApi(base_url=get_api_url(),
621 username='admin',
622 password=db.get('admin_password'),
623 token_name='graylog-charm')
624@@ -393,22 +453,17 @@ def graylog_remove_filebeat_input():
625 remove_state('beat.setup')
626
627
628+@when('graylog.configured')
629+@when('graylog_api.configured')
630 @when('beats.connected')
631-def provide_beats_port(filebeat):
632- beats_port = hookenv.config('beats_port')
633- filebeat.provide_data(beats_port)
634- set_state('beat.setup')
635-
636-
637-@when('beat.setup')
638-def setup_beats_input():
639-
640+@when_not('beat.setup')
641+def setup_beats_input(filebeat):
642 port = hookenv.config('beats_port')
643 hookenv.log("Setting up beats input on port {}".format(port))
644+ filebeat.provide_data(port)
645
646 db = unitdata.kv()
647-
648- g = GraylogApi(base_url=API_URL,
649+ g = GraylogApi(base_url=get_api_url(),
650 username='admin',
651 password=db.get('admin_password'),
652 token_name='graylog-charm')
653@@ -444,7 +499,10 @@ def setup_beats_input():
654 else:
655 hookenv.log("beats input already configured")
656
657+ set_state('beat.setup')
658+
659
660+@when('graylog.configured')
661 @when('graylog_api.configured')
662 @when_not('graylog_inputs.configured')
663 def configure_inputs(*discard):
664@@ -453,7 +511,7 @@ def configure_inputs(*discard):
665 """
666 conf = hookenv.config()
667 db = unitdata.kv()
668- g = GraylogApi(base_url=API_URL,
669+ g = GraylogApi(base_url=get_api_url(),
670 username='admin',
671 password=db.get('admin_password'),
672 token_name='graylog-charm')
673@@ -569,8 +627,9 @@ def update_reverseproxy_config(website):
674 url = urlparse(conf['web_listen_uri'])
675 website.configure(port=url.port)
676 services += "- {service_name: web, service_port: " + str(url.port) + "}\n"
677-
678- services += "- {service_name: api, service_port: " + API_PORT + "}\n"
679+ if is_v2():
680+ # NB: there is no revproxy to the api endpoint in v3+.
681+ services += "- {service_name: api, service_port: " + get_api_port() + "}\n"
682 website.set_remote(all_services=services)
683
684
685@@ -692,16 +751,17 @@ def set_jvm_heap_size(heap_size='1G', conf_path=SERVER_DEFAULT_CONF_FILE):
686
687 @when('graylog.configured')
688 @when('nrpe-external-master.available')
689+@when_not('graylog_nagios.configured')
690 def configure_nagios(nagios):
691- if hookenv.hook_name() == 'update-status':
692- return
693-
694 db = unitdata.kv()
695 nagios_password = db.get('nagios_password')
696 if not nagios_password:
697 nagios_password = host.pwgen(18)
698 db.set('nagios_password', nagios_password)
699
700+ api_port = get_api_port()
701+ api_url = get_api_url()
702+
703 # Ask charmhelpers.contrib.charmsupport's nrpe to work out our hostname
704 hostname = nrpe.get_nagios_hostname()
705 nrpe_setup = nrpe.NRPE(hostname=hostname, primary=True)
706@@ -728,11 +788,11 @@ def configure_nagios(nagios):
707 nrpe_setup.add_check(
708 'graylog_api',
709 'Greylog API check',
710- '/usr/lib/nagios/plugins/check_http -I 127.0.0.1 -p {} -u /api -s "{}"'.format(API_PORT, check_string)
711+ '/usr/lib/nagios/plugins/check_http -I 127.0.0.1 -p {} -u /api -s "{}"'.format(api_port, check_string)
712 )
713
714 # For nagios checks via API, we need to create a user and token
715- g = GraylogApi(base_url=API_URL,
716+ g = GraylogApi(base_url=api_url,
717 username='admin',
718 password=db.get('admin_password'),
719 token_name='graylog-charm')
720@@ -740,7 +800,7 @@ def configure_nagios(nagios):
721 g.user_permissions_set('nagios', ['users:tokenlist', 'users:tokencreate',
722 'indexercluster:read', 'indices:failures',
723 'notifications:read', 'journal:read'])
724- g = GraylogApi(base_url=API_URL,
725+ g = GraylogApi(base_url=api_url,
726 username='nagios',
727 password=nagios_password,
728 token_name='nagios')
729@@ -764,7 +824,8 @@ def configure_nagios(nagios):
730 os.path.join(nagios_plugins, 'check_graylog_health.py'),
731 conf.get('nagios_uncommitted_warn', 0),
732 conf.get('nagios_uncommitted_crit', 100),
733- API_URL)
734+ api_url)
735 )
736
737 nrpe_setup.write()
738+ set_state('graylog_nagios.configured')
739diff --git a/unit_tests/test_graylog.py b/unit_tests/test_graylog.py
740index 38ac6fa..37fb78d 100644
741--- a/unit_tests/test_graylog.py
742+++ b/unit_tests/test_graylog.py
743@@ -3,20 +3,169 @@ import sys
744 import tempfile
745 import unittest
746 from unittest import mock
747+from charms.reactive import set_state, is_state
748
749-from charms.layer.graylog.api import GraylogApi
750-
751-# charms.leadership only exists in the built charm; mock it out before
752-# the graylog imports since those depend on charms.leadership
753-layer_mock = mock.Mock()
754-sys.modules['charms.leadership'] = layer_mock
755+# some dep layers only exists in the built charm; mock them out before
756+# the graylog imports since those depend on post-build charms.* layers
757+leader_mock = mock.Mock()
758+sys.modules['charms.leadership'] = leader_mock
759+snap_mock = mock.Mock()
760+sys.modules['charms.layer.snap'] = snap_mock
761
762 from reactive.graylog import (
763+ GraylogApi,
764+ configure_graylog,
765+ install_graylog,
766+ refresh_graylog,
767 set_conf,
768 set_jvm_heap_size,
769+ update_config,
770+ upgrade_charm,
771 _check_input_exists) # noqa: E402
772 from files import check_graylog_health # noqa: E402
773
774+
775+class TestInstallUpdateUpgrade(unittest.TestCase):
776+ """Test that install / update / upgrade clears our configured flag."""
777+
778+ @mock.patch('reactive.graylog.snap')
779+ @mock.patch('reactive.graylog.hookenv.config')
780+ def test_install(self, mock_config, mock_snap):
781+ class Config(dict):
782+ def __init__(self, *args, **kw):
783+ super(Config, self).__init__(*args, **kw)
784+
785+ mock_config.return_value = Config({'channel': '2/stable'})
786+ install_graylog()
787+ self.assertTrue(mock_snap.install.called)
788+ self.assertFalse(is_state('graylog.configured'))
789+
790+ def test_update(self):
791+ set_state('graylog.configured')
792+ update_config()
793+ self.assertFalse(is_state('graylog.configured'))
794+
795+ def test_upgrade(self):
796+ set_state('graylog.configured')
797+ upgrade_charm()
798+ self.assertFalse(is_state('graylog.configured'))
799+
800+
801+class TestRefresh(unittest.TestCase):
802+ """Test the snap refresh handler."""
803+
804+ @mock.patch('reactive.graylog.snap')
805+ @mock.patch('reactive.graylog.hookenv.config')
806+ def test_no_channel(self, mock_config, mock_snap):
807+ """Ensure a no-op if there is no 'channel' config."""
808+ mock_config.return_value = ''
809+ mock_snap.get_installed_version.return_value = '2.foo'
810+ set_state('graylog.configured')
811+ refresh_graylog()
812+ self.assertFalse(mock_snap.refresh.called)
813+ self.assertTrue(is_state('graylog.configured'))
814+
815+ @mock.patch('reactive.graylog.shutil')
816+ @mock.patch('reactive.graylog.os')
817+ @mock.patch('reactive.graylog.host')
818+ @mock.patch('reactive.graylog.snap')
819+ @mock.patch('reactive.graylog.hookenv.config')
820+ def test_no_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil):
821+ """Ensure no reconfiguration if the snap version does not change."""
822+ mock_config.return_value = 'foo'
823+ mock_snap.get_installed_version.side_effect = ['2.foo', '2.foo']
824+ mock_os.path.exists.return_value = True
825+ mock_shutil.copy2.return_value = 'foo'
826+ set_state('graylog.configured')
827+ refresh_graylog()
828+ self.assertTrue(mock_snap.refresh.called)
829+ self.assertTrue(is_state('graylog.configured'))
830+
831+ @mock.patch('reactive.graylog.data_changed')
832+ @mock.patch('reactive.graylog.shutil.copy2')
833+ @mock.patch('reactive.graylog.os')
834+ @mock.patch('reactive.graylog.host')
835+ @mock.patch('reactive.graylog.snap')
836+ @mock.patch('reactive.graylog.hookenv.config')
837+ def test_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil, mock_data_changed):
838+ """Ensure calls and states when the snap has changed."""
839+ mock_config.return_value = 'foo'
840+ mock_snap.get_installed_version.side_effect = ['2.foo', '3.foo']
841+ mock_os.path.exists.return_value = True
842+ mock_shutil.return_value = 'foo'
843+ set_state('graylog.configured')
844+ refresh_graylog()
845+ self.assertTrue(mock_host.service_stop.called)
846+ self.assertTrue(mock_snap.refresh.called)
847+ self.assertEqual(mock_shutil.call_count, 2)
848+ self.assertEqual(mock_data_changed.call_count, 2)
849+ self.assertFalse(is_state('graylog.configured'))
850+
851+
852+class TestConfig(unittest.TestCase):
853+ """Test the configure_graylog handler."""
854+
855+ @mock.patch('reactive.graylog.hookenv')
856+ @mock.patch('reactive.graylog.os')
857+ def test_no_conf_file(self, mock_os, mock_hookenv):
858+ """Ensure a status message if there is no config file present."""
859+ mock_os.path.exists.return_value = False
860+ configure_graylog()
861+ self.assertTrue(mock_hookenv.status_set.called)
862+
863+ @mock.patch('reactive.graylog.set_jvm_heap_size')
864+ @mock.patch('reactive.graylog.validate_api_uri')
865+ @mock.patch('reactive.graylog.is_v2')
866+ @mock.patch('reactive.graylog.set_conf')
867+ @mock.patch('reactive.graylog.hookenv')
868+ @mock.patch('reactive.graylog.os')
869+ @mock.patch('reactive.graylog.unitdata')
870+ def test_v2_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, mock_validate_uri, mock_jvm_heap):
871+ """Ensure calls and states with various config values for v2."""
872+ class Config(dict):
873+ def __init__(self, *args, **kw):
874+ super(Config, self).__init__(*args, **kw)
875+
876+ mock_ud.kv.return_value = {'admin_password': 'password', 'password_secret': 'secret'}
877+ mock_os.path.exists.return_value = True
878+ mock_hookenv.config.return_value = Config({'jvm_heap_size': '1G',
879+ 'web_listen_uri': None,
880+ 'rest_transport_uri': 'foo/api',
881+ 'web_endpoint_uri': 'bar/api'})
882+ mock_v2.return_value = True
883+ configure_graylog()
884+ self.assertEqual(mock_validate_uri.call_count, 2)
885+ self.assertTrue(mock_jvm_heap.called)
886+ self.assertTrue(mock_hookenv.application_version_set.called)
887+ self.assertTrue(mock_hookenv.open_port.called)
888+
889+ @mock.patch('reactive.graylog.set_jvm_heap_size')
890+ @mock.patch('reactive.graylog.validate_api_uri')
891+ @mock.patch('reactive.graylog.is_v2')
892+ @mock.patch('reactive.graylog.set_conf')
893+ @mock.patch('reactive.graylog.hookenv')
894+ @mock.patch('reactive.graylog.os')
895+ @mock.patch('reactive.graylog.unitdata')
896+ def test_v3_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, mock_validate_uri, mock_jvm_heap):
897+ """Ensure calls and states with various config values for v3."""
898+ class Config(dict):
899+ def __init__(self, *args, **kw):
900+ super(Config, self).__init__(*args, **kw)
901+
902+ mock_ud.kv.return_value = {'admin_password': 'password', 'password_secret': 'secret'}
903+ mock_os.path.exists.return_value = True
904+ mock_hookenv.config.return_value = Config({'jvm_heap_size': '1G',
905+ 'web_listen_uri': None,
906+ 'rest_transport_uri': 'foo/api',
907+ 'web_endpoint_uri': 'bar/api'})
908+ mock_v2.return_value = False
909+ configure_graylog()
910+ self.assertEqual(mock_validate_uri.call_count, 2)
911+ self.assertTrue(mock_jvm_heap.called)
912+ self.assertTrue(mock_hookenv.application_version_set.called)
913+ self.assertTrue(mock_hookenv.open_port.called)
914+
915+
916 initial_conf = u"""#key1 = value1
917 key2 = value2
918 key2=value3
919diff --git a/unit_tests/test_lib.py b/unit_tests/test_lib.py
920new file mode 100644
921index 0000000..565ffc1
922--- /dev/null
923+++ b/unit_tests/test_lib.py
924@@ -0,0 +1,36 @@
925+import unittest
926+from unittest import mock
927+
928+from charms.layer.graylog import (
929+ get_api_port,
930+ get_api_url,
931+ validate_api_uri,
932+)
933+
934+
935+class TestLibraryUtils(unittest.TestCase):
936+ """Test lib.charms.layer.graylog utils."""
937+
938+ @mock.patch('charms.layer.graylog.snap.get_installed_version')
939+ def test_api(self, mock_snap):
940+ """Validate the api port/url match what we expect for v2 and v3."""
941+ mock_snap.return_value = '2.foo'
942+ self.assertEqual(get_api_port(), '9001')
943+ self.assertEqual(get_api_url(), 'http://127.0.0.1:9001/api/')
944+
945+ mock_snap.return_value = '3.bar'
946+ self.assertEqual(get_api_port(), '9000')
947+ self.assertEqual(get_api_url(), 'http://127.0.0.1:9000/api/')
948+
949+ @mock.patch('charms.layer.graylog.snap.get_installed_version')
950+ def test_validate_api_url(self, mock_snap):
951+ """Validate the URI suffix is valid for v2 and v3."""
952+ mock_snap.return_value = '2.foo'
953+ self.assertEqual(validate_api_uri('foo'), 'foo/api/')
954+ self.assertEqual(validate_api_uri('foo/'), 'foo/api/')
955+ self.assertEqual(validate_api_uri('foo/api'), 'foo/api/')
956+
957+ mock_snap.return_value = '3.bar'
958+ self.assertEqual(validate_api_uri('foo'), 'foo/')
959+ self.assertEqual(validate_api_uri('foo/'), 'foo/')
960+ self.assertEqual(validate_api_uri('foo/api/'), 'foo/')
961diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py
962index d3ea072..5bae816 100644
963--- a/unit_tests/test_logextract.py
964+++ b/unit_tests/test_logextract.py
965@@ -1,7 +1,7 @@
966 import unittest
967 from unittest import mock
968
969-from charms.layer.graylog import logextract
970+from charms.layer.graylog import GraylogPipelines, GraylogRules, GraylogStreams, LogExtractPipeline
971
972
973 testdata_simple_pipeline_source = '\npipeline "testpipe"\nstage 1 match either\n rule "test-rule";\nend\n'
974@@ -47,14 +47,14 @@ class TestGraylogPipelines(unittest.TestCase):
975 @mock.patch('charms.layer.graylog.api.GraylogApi')
976 def test_get_pipeline_for(self, mock_api):
977 mock_api.request.return_value = testdata_simple_pipeline
978- gpipes = logextract.GraylogPipelines(mock_api)
979+ gpipes = GraylogPipelines(mock_api)
980 test = gpipes.get_for_title('juju-logextract-pipeline')
981 self.assertEqual(test['title'], 'juju-logextract-pipeline')
982
983 @mock.patch('charms.layer.graylog.api.GraylogApi')
984 def test_set_pipeline(self, mock_api):
985 mock_api.request.return_value = testdata_simple_pipeline
986- gpipes = logextract.GraylogPipelines(mock_api)
987+ gpipes = GraylogPipelines(mock_api)
988 gpipes.set('juju-logextract-pipeline', testdata_simple_pipeline_source)
989 self.assertEqual(mock_api.request.call_args_list[1][1]['data']['title'], 'juju-logextract-pipeline')
990
991@@ -64,7 +64,7 @@ class TestGraylogRules(unittest.TestCase):
992 @mock.patch('charms.layer.graylog.api.GraylogApi')
993 def test_get_rules(self, mock_api):
994 mock_api.request.return_value = testdata_rules
995- grules = logextract.GraylogRules(mock_api)
996+ grules = GraylogRules(mock_api)
997 rules = grules.get()
998 self.assertEqual(rules[0]['title'], "from ceph mon")
999
1000@@ -74,7 +74,7 @@ class TestGraylogStreams(unittest.TestCase):
1001 @mock.patch('charms.layer.graylog.api.GraylogApi')
1002 def test_get_stream_id_for(self, mock_api):
1003 mock_api.request.return_value = testdata_streams
1004- gstreams = logextract.GraylogStreams(mock_api)
1005+ gstreams = GraylogStreams(mock_api)
1006 allmsgs = gstreams.get_stream_id_for("All messages")
1007 self.assertEqual(allmsgs, testdata_streams['streams'][0]['id'])
1008 nope = gstreams.get_stream_id_for("I don't exist")
1009@@ -88,13 +88,13 @@ class TestLogExtractPipeline(unittest.TestCase):
1010 mock_api.request.return_value = testdata_simple_pipeline
1011
1012 def test_parse_rule_title(self):
1013- logext = logextract.LogExtractPipeline()
1014+ logext = LogExtractPipeline()
1015 t = logext.parse_rule_title(testdata_rules[0]['source'])
1016 self.assertEqual(t, 'from ceph mon')
1017
1018 @mock.patch('charms.layer.graylog.api.GraylogApi')
1019 def test_set_rules(self, mock_api):
1020- logext = logextract.LogExtractPipeline(mock_api)
1021+ logext = LogExtractPipeline(mock_api)
1022 logext.set_rules([testdata_rules[0]['source']])
1023 exp = [mock.call('/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline')]
1024 mock_api.request.assert_has_calls(exp)
1025diff --git a/wheelhouse.txt b/wheelhouse.txt
1026new file mode 100644
1027index 0000000..d555101
1028--- /dev/null
1029+++ b/wheelhouse.txt
1030@@ -0,0 +1 @@
1031+requests>=2.0.0,<3.0.0

Subscribers

People subscribed via source and target branches

to all changes: