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
diff --git a/README.md b/README.md
index a8174c7..94a4b69 100644
--- a/README.md
+++ b/README.md
@@ -1,65 +1,101 @@
1# Overview1# Overview
22
3The charm installs [Graylog](https://www.graylog.org/) using the [snap package](https://uappexplorer.com/snap/ubuntu/graylog).3The charm installs [Graylog](https://www.graylog.org/) using the [snap package](https://snapcraft.io/graylog).
4The charm must be related to elasticsearch and mongodb in order to be a fully functioning installation.
54
6# Usage5## Usage
76
7```bash
8juju deploy cs:~graylog-charmers/graylog8juju deploy cs:~graylog-charmers/graylog
9juju run-action graylog/X show-admin-password9juju run-action --wait graylog/X show-admin-password
10juju show-action-output <action ID>10```
1111
12Graylog requires MongoDB to run and Elasticsearch to be useful.12Graylog requires MongoDB to run and Elasticsearch to be useful.
1313
14juju deploy cs:mongodb14```bash
15juju deploy cs:~mongodb-charmers/mongodb
15juju relate graylog:mongodb mongodb:database16juju relate graylog:mongodb mongodb:database
17
16juju deploy cs:~elasticsearch-charmers/elasticsearch18juju deploy cs:~elasticsearch-charmers/elasticsearch
17juju relate graylog:elasticsearch elasticsearch:client19juju relate graylog:elasticsearch elasticsearch:client
20```
1821
19You can then browse to http://ip-address:9000 and log in as the user "admin".22You can then browse to `http://ip-address:9000` and log in as the user "admin".
20The 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.23The password is by default a random value so `juju run-action --wait graylog/X show-admin-password` must be run for
24admin access to the installation.
2125
22## Reverseproxy Relation26## Reverseproxy Relation
23Graylog supports advertising its web and api ports to an application acting as a reverseproxy using the http relation.
24The 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.
25More details on using this are in the reverseproxy instructions for the [Apache2 charm](https://jujucharms.com/apache2/).
2627
27For example, you could use the following as a graylog vhost template for the apache2 charm.28Graylog supports advertising its ports to an application acting as a reverseproxy using the `http` relation. The port
28Note that you'll need to update the GRAYLOG_UNIT_IP in the template below to match the IP of your graylog/X unit.29of the webUI is exposed over the relation in the `all_services` variable of the relation.
2930
30```31>**Note**: For Graylog version 2, the API port is also exposed over the `http` relation. Graylog version 3 hard codes
32the `/api/` location and uses the default port (9000) for both webUI and API.
33
34More details on using this are in the reverseproxy instructions for the
35[Apache2 charm](https://jujucharms.com/apache2/).
36
37Sample Graylog 2 vhost template for the apache2 charm:
38
39```bash
31$ cat graylog-vhost.tmpl40$ cat graylog-vhost.tmpl
32<Location "/">41<Location "/">
33 RequestHeader set X-Graylog-Server-URL "http://{{servername}}/api/"42 RequestHeader set X-Graylog-Server-URL "http://{{servername}}/api/"
34 ProxyPass http://GRAYLOG_UNIT_IP:9000/43 ProxyPass http://{{graylog_web}}/
35 ProxyPassReverse http://GRAYLOG_UNIT_IP:9000/44 ProxyPassReverse http://{{graylog_web}}/
36</Location>45</Location>
3746
38<Location "/api/">47<Location "/api/">
39 ProxyPass http://GRAYLOG_UNIT_IP:9001/api/48 ProxyPass http://{{graylog_api}}/api/
40 ProxyPassReverse http://GRAYLOG_UNIT_IP:9001/api/49 ProxyPassReverse http://{{graylog_api}}/api/
41</Location>50</Location>
42```51```
4352
44Now deploy and configure apache2 as your graylog reverse proxy:53Sample Graylog 3 vhost template for the apache2 charm:
4554
55```bash
56$ cat graylog-vhost.tmpl
57<Location "/">
58 RequestHeader set X-Graylog-Server-URL "http://{{servername}}/"
59 ProxyPass http://{{graylog_web}}/
60 ProxyPassReverse http://{{graylog_web}}/
61</Location>
46```62```
63
64Now deploy and configure apache2 as your graylog reverse proxy:
65
66```bash
47juju deploy apache267juju deploy apache2
48juju config apache2 "enable_modules='headers proxy_html proxy_http'"68juju config apache2 "enable_modules='headers proxy_html proxy_http'"
49juju config apache2 "vhost_http_template=$(base64 < graylog-vhost.tmpl)"69juju config apache2 "vhost_http_template=$(base64 ./graylog-vhost.tmpl)"
50juju expose apache270juju expose apache2
51juju relate apache2:reverseproxy graylog:website71juju relate apache2:reverseproxy graylog:website
52```72```
5373
54Visit http://[apache2-public-ip] to access the Graylog interface.74Visit `http://<apache2-public-ip>` to access the Graylog web interface.
5575
56## Scale out Usage76## Scale out Usage
5777
58The MongoDB and Elasticsearch applications can both be scaled to clusters and Graylog will adapt to using the cluster.78The MongoDB and Elasticsearch applications can both be scaled up or down. Graylog will reconfigure itself as needed.
59The Graylog charm does not yet support clustering of multiple units.79The Graylog charm does not yet support clustering of multiple units.
6080
61# Configuration81## Configuration
82
83Depending on the Elasticsearch charm used, the cluster name may not be passed to Graylog. In this case, the
84`elasticsearch_cluster_name` config option should be set.
85
86## Upgrade
87
88Graylog may be upgraded to a different snap version by setting the `channel` config option. For example, switch to the
89latest version 3 edge snap with the following:
90
91```bash
92juju config graylog channel='3/edge'
93```
6294
63The 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.95>**Note**: When upgrading from Graylog version 2 to version 3, please consult the
96[upgrade guide](https://docs.graylog.org/en/3.1/pages/upgrade.html) to ensure your environment meets the minimum
97requirements.
6498
65Depending 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.99If a new `channel` config option results in a new snap being installed, the charm will backup the previous
100configuration file on the graylog unit in `/var/snap/graylog/common/server.conf.$prev`. This may be useful if
101graylog needs to be reverted to a previous version in the future.
diff --git a/config.yaml b/config.yaml
index 77bc734..d0d2649 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,4 +1,11 @@
1options:1options:
2 channel:
3 type: string
4 default: "2/stable"
5 description: |
6 Snap channel used to install/refresh the graylog snap.
7
8 This option has no effect when a valid graylog.snap resource is attached.
2 elasticsearch_cluster_name:9 elasticsearch_cluster_name:
3 type: string10 type: string
4 default: ""11 default: ""
@@ -6,24 +13,38 @@ options:
6 web_listen_uri:13 web_listen_uri:
7 type: string14 type: string
8 default: "http://0.0.0.0:9000/"15 default: "http://0.0.0.0:9000/"
9 description: The uri the web interface will be available at.16 description: |
17 The uri the web interface will be available at. In version 3 and higher,
18 this is used for and converted to the appropriate format for
19 the 'http_bind_address' config value.
10 web_endpoint_uri:20 web_endpoint_uri:
11 type: string21 type: string
12 default: ""22 default: ""
13 description: >23 description: |
14 If set, this will be published as the external address for connecting REST24 If set, this will be published as the external address for connecting to
15 API of the Graylog server. Web interface clients need to be able to25 the REST API of the Graylog server. Web interface clients need to be able
16 connect to this for the web interface to work.26 to connect to this for the web interface to work.
17 By default, will use `rest_transport_uri` if defined, or will select one27
18 of the node's allocated ips on port 9001. Example: http://10.0.0.1:9001/28 In version 2, Graylog will set the default value to 'rest_transport_uri'
29 if defined; otherwise it will select the first non-loopback IPv4 address
30 on port 9001. Example: http://10.0.0.1:9001/
31
32 In version 3, Graylog refers to this option as 'http_external_uri' with
33 the default value being 'rest_transport_uri' if defined; otherwise it will
34 select the first non-loopback IPv4 address on port 9000.
35 Example: http://10.0.0.1:9000/
19 rest_transport_uri:36 rest_transport_uri:
20 type: string37 type: string
21 default: ""38 default: ""
22 description: >39 description: |
23 If set, this will be promoted in the cluster discovery APIs. You will need40 If set, this will be promoted in the cluster discovery APIs. You will need
24 to define this, if your Graylog server is running behind a HTTP proxy that41 to define this if your Graylog server is running behind a HTTP proxy that
25 is rewriting the scheme, host name or URI. This must not contain a42 is rewriting the scheme, host name, or URI. This must not contain a
26 wildcard address (0.0.0.0). Example: http://192.168.1.1:9001/43 wildcard address (0.0.0.0).
44
45 For Graylog 2, this usually takes the form http://10.0.0.1:9001/api/.
46 For Graylog 3 and higher, this is known as the 'http_publish_uri' and looks
47 like http://10.0.0.1:9000/.
27 index_replicas:48 index_replicas:
28 type: int49 type: int
29 default: 050 default: 0
diff --git a/layer.yaml b/layer.yaml
index d4fa469..414c56e 100644
--- a/layer.yaml
+++ b/layer.yaml
@@ -8,6 +8,11 @@ includes:
8 - 'interface:http'8 - 'interface:http'
9 - 'interface:mongodb-cluster'9 - 'interface:mongodb-cluster'
10 - 'interface:nrpe-external-master'10 - 'interface:nrpe-external-master'
11exclude:
12 - .coverage
13 - .tox
14 - .unit-state.db
15 - __pycache__
11options:16options:
12 basic:17 basic:
13 packages:18 packages:
@@ -15,5 +20,3 @@ options:
15 snap:20 snap:
16 core:21 core:
17 channel: stable22 channel: stable
18 graylog:
19 channel: stable
diff --git a/lib/charms/layer/graylog/__init__.py b/lib/charms/layer/graylog/__init__.py
index e69de29..5399010 100644
--- a/lib/charms/layer/graylog/__init__.py
+++ b/lib/charms/layer/graylog/__init__.py
@@ -0,0 +1,3 @@
1from .api import GraylogApi # NOQA
2from .logextract import GraylogPipelines, GraylogRules, GraylogStreams, LogExtractPipeline # NOQA
3from .utils import * # NOQA
diff --git a/lib/charms/layer/graylog/logextract.py b/lib/charms/layer/graylog/logextract.py
index 2447da4..5d1ae27 100644
--- a/lib/charms/layer/graylog/logextract.py
+++ b/lib/charms/layer/graylog/logextract.py
@@ -3,6 +3,7 @@ extraction
3"""3"""
44
5import re5import re
6from .utils import is_v2
67
78
8class GraylogTitleSourceItems:9class GraylogTitleSourceItems:
@@ -47,11 +48,13 @@ class GraylogTitleSourceItems:
4748
4849
49class GraylogPipelines(GraylogTitleSourceItems):50class GraylogPipelines(GraylogTitleSourceItems):
50 url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline'51 url = '{}/system/pipelines/pipeline'.format(
52 '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
5153
5254
53class GraylogRules(GraylogTitleSourceItems):55class GraylogRules(GraylogTitleSourceItems):
54 url = '/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/rule'56 url = '{}/system/pipelines/rule'.format(
57 '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
5558
5659
57class GraylogStreams:60class GraylogStreams:
@@ -78,7 +81,9 @@ class GraylogStreams:
78 :param pipeline title81 :param pipeline title
79 :param stream title82 :param stream title
80 """83 """
81 url = "/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/connections/to_stream"84 url = '{}/system/pipelines/connections/to_stream'.format(
85 '/plugins/org.graylog.plugins.pipelineprocessor' if is_v2() else '')
86
82 streamid = self.get_stream_id_for(stream)87 streamid = self.get_stream_id_for(stream)
83 pipes = GraylogPipelines(self.graylogapi)88 pipes = GraylogPipelines(self.graylogapi)
84 pipeline = pipes.get_for_title(pipeline)89 pipeline = pipes.get_for_title(pipeline)
diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py
85new file mode 10064490new file mode 100644
index 0000000..cf89b61
--- /dev/null
+++ b/lib/charms/layer/graylog/utils.py
@@ -0,0 +1,42 @@
1from charmhelpers.core import hookenv
2from charms.layer import snap
3
4SNAP_NAME = 'graylog'
5
6
7def get_api_port():
8 return '9001' if is_v2() else '9000'
9
10
11def get_api_url():
12 return 'http://127.0.0.1:{}/api/'.format(get_api_port())
13
14
15def is_v2():
16 version = snap.get_installed_version(SNAP_NAME)
17 # If the snap isn't installed yet, base our version off the charm config
18 if not version:
19 version = hookenv.config('channel')
20 return True if version.startswith('2') else False
21
22
23def validate_api_uri(uri):
24 """Ensure the given API URI is valid based on the Graylog version.
25
26 For v2, api-related options should end with '/api/'. In v3, the graylog
27 server hard codes '/api/' to the end of these options and should therefore
28 be removed if included in the charm config.
29 """
30 if not uri.endswith('/'):
31 uri += '/'
32
33 if is_v2():
34 if not uri.endswith('/api/'):
35 hookenv.log("Appending 'api/' to the configured REST API options")
36 uri += 'api/'
37 else:
38 if uri.endswith('/api/'):
39 hookenv.log("Removing 'api/' from the configurted REST API options")
40 uri = uri[:-4]
41
42 return uri
diff --git a/reactive/graylog.py b/reactive/graylog.py
index b07e376..17427b4 100644
--- a/reactive/graylog.py
+++ b/reactive/graylog.py
@@ -1,23 +1,31 @@
1import hashlib1import hashlib
2import os2import os
3import re3import re
4import shutil
4import time5import time
5import yaml6import yaml
6from subprocess import check_output, CalledProcessError
7from urllib.parse import urlparse7from urllib.parse import urlparse
88
9from charms.reactive import hook, when, when_not, is_state, remove_state, set_state9from charms.reactive import hook, when, when_any, when_not, when_not_all, is_state, remove_state, set_state
10from charms.reactive.helpers import data_changed10from charms.reactive.helpers import data_changed
11from charmhelpers.core import host, hookenv, unitdata11from charmhelpers.core import host, hookenv, unitdata
12from charmhelpers.contrib.charmsupport import nrpe12from charmhelpers.contrib.charmsupport import nrpe
13import charms.leadership13import charms.leadership
1414
15from charms.layer.graylog import logextract15from charms.layer import snap
16from charms.layer.graylog.api import GraylogApi16from charms.layer.graylog import (
17 GraylogApi,
18 LogExtractPipeline,
19 SNAP_NAME,
20 get_api_port,
21 get_api_url,
22 is_v2,
23 validate_api_uri
24)
25
1726
18API_PORT = '9001' # This is the default set by the snap
19API_URL = 'http://127.0.0.1:9001/api/' # This is the default set by the snap
20CONF_FILE = '/var/snap/graylog/common/server.conf'27CONF_FILE = '/var/snap/graylog/common/server.conf'
28SNAP_CONF_FILE = '/snap/graylog/current/etc/graylog/server/server.conf'
21# /snap/graylog is a read-only squashfs so we use the initial settings29# /snap/graylog is a read-only squashfs so we use the initial settings
22# and write out an override to SERVER_DEFAULT_CONF_FILE30# and write out an override to SERVER_DEFAULT_CONF_FILE
23SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server'31SHIPPED_SNAP_SERVER_DEFAULT_CONF_FILE = '/snap/graylog/current/etc/default/graylog-server'
@@ -32,7 +40,13 @@ def upgrade_charm():
3240
3341
34@when('config.changed')42@when('config.changed')
43@when_not('config.changed.channel')
35def update_config():44def update_config():
45 """Allow new runtime config options to be processed.
46
47 Reset flags to allow new runtime config options to be written. Deployment
48 config options (e.g. 'channel') are handled separately.
49 """
36 remove_state('graylog.configured')50 remove_state('graylog.configured')
3751
3852
@@ -55,63 +69,105 @@ rotation_strategies = {
55}69}
5670
5771
72@when_not('snap.installed.graylog')
73def install_graylog():
74 """Install the graylog snap.
75
76 The snap layer will first try to install a valid 'graylog.snap' resource.
77 If a valid resource is not found, the snap will be installed from the snap
78 store according to the configured 'channel' option.
79 """
80 channel = hookenv.config('channel')
81
82 hookenv.status_set('maintenance', 'Installing graylog snap')
83 snap.install(SNAP_NAME, channel=channel)
84
85
86@when('snap.installed.graylog')
87@when('config.changed.channel')
88def refresh_graylog():
89 """Refresh the graylog snap.
90
91 A change to the 'channel' config option may warrant a snap refresh. If so,
92 stop the service, backup the current config, and refresh.
93 """
94 channel = hookenv.config('channel')
95 cur_snap = new_snap = snap.get_installed_version(SNAP_NAME)
96
97 if channel:
98 host.service_stop(SERVICE_NAME)
99 # backup config file using the current version as a suffix
100 if os.path.exists(CONF_FILE):
101 shutil.copy2(CONF_FILE, '{path}.{ext}'.format(path=CONF_FILE, ext=cur_snap))
102
103 hookenv.status_set('maintenance', 'Refreshing graylog snap {}'.format(channel))
104 snap.refresh(SNAP_NAME, channel=channel)
105 new_snap = snap.get_installed_version(SNAP_NAME)
106 else:
107 hookenv.log("Cannot refresh snap when the 'channel' config option is missing")
108
109 # When the snap version changes, use the config file from the snap and
110 # adjust states to ensure appropriate re-config happens.
111 if cur_snap != new_snap:
112 shutil.copy2(SNAP_CONF_FILE, CONF_FILE)
113 data_changed('elasticsearch.relation', None)
114 data_changed('mongodb.uri', None)
115 remove_state('graylog.configured')
116
117 report_status()
118
119
58@when('snap.installed.graylog') # noqa: C901120@when('snap.installed.graylog') # noqa: C901
59@when_not('graylog.configured')121@when_not('graylog.configured')
60def configure_graylog():122def configure_graylog():
61 db = unitdata.kv()123 db = unitdata.kv()
62 if not os.path.exists(CONF_FILE):124 if not os.path.exists(CONF_FILE):
63 hookenv.log('Configuration file "{}" missing, skipping configuration run.'.format(CONF_FILE))125 hookenv.status_set('maintenance', 'Waiting for {}'.format(CONF_FILE))
64 hookenv.status_set('waiting', 'Waiting for snap config: {}'.format(CONF_FILE))
65 return126 return
66 conf = hookenv.config()127 conf = hookenv.config()
128
67 admin_password = db.get('admin_password')129 admin_password = db.get('admin_password')
68 if not admin_password:130 password_secret = db.get('password_secret')
69 admin_password = host.pwgen(18)131 if not (admin_password and password_secret):
70 db.set('admin_password', admin_password)132 hookenv.status_set('maintenance', 'Waiting for leader passwords')
133 return
71 pw_hash = hashlib.sha256(admin_password.encode('utf-8')).hexdigest()134 pw_hash = hashlib.sha256(admin_password.encode('utf-8')).hexdigest()
72 set_conf('root_password_sha2', pw_hash)135 set_conf('root_password_sha2', pw_hash)
136 set_conf('password_secret', password_secret)
73137
74 if conf['web_listen_uri']:138 api_port = get_api_port()
75 url = urlparse(conf['web_listen_uri'])139 if is_v2():
76 webport = db.get('web_listen_port')140 if conf['web_listen_uri']:
141 url = urlparse(conf['web_listen_uri'])
142 webport = db.get('web_listen_port')
77143
78 if webport == API_PORT:144 if webport == api_port:
79 msg = "Config 'web_listen_uri' is using port {} which conflicts with default REST port {}".format(145 msg = "Config 'web_listen_uri' is using port {} which conflicts with default REST port {}".format(
80 webport, API_PORT146 webport, api_port
81 )147 )
82 hookenv.status_set('blocked', msg)148 hookenv.status_set('blocked', msg)
83 return149 return
150
151 set_conf('web_listen_uri', conf['web_listen_uri'])
152 if url.port != webport:
153 if webport:
154 hookenv.close_port(webport)
155 db.set('web_listen_port', url.port)
156 hookenv.open_port(url.port)
84157
85 set_conf('web_listen_uri', conf['web_listen_uri'])158 if conf['rest_transport_uri']:
86 if url.port != webport:159 set_conf('rest_transport_uri', validate_api_uri(conf['rest_transport_uri']))
87 if webport:160
88 hookenv.close_port(webport)161 if conf['web_endpoint_uri']:
89 db.set('web_listen_port', url.port)162 set_conf('web_endpoint_uri', validate_api_uri(conf['web_endpoint_uri']))
90 hookenv.open_port(url.port)
91
92 if conf['rest_transport_uri']:
93 set_conf('rest_transport_uri', _validate_api_uri(conf['rest_transport_uri']))
94
95 if conf['web_endpoint_uri']:
96 set_conf('web_endpoint_uri', _validate_api_uri(conf['web_endpoint_uri']))
97
98 hookenv.open_port(API_PORT)
99
100 # Set application version
101 snap_name = "graylog"
102 snap_version = ""
103 cmd = ['snap', 'list', snap_name]
104 try:
105 out = check_output(cmd).decode('UTF-8')
106 except CalledProcessError:
107 pass
108 else:163 else:
109 lines = out.split('\n')164 if conf['web_listen_uri']:
110 for line in lines:165 url = urlparse(conf['web_listen_uri'])
111 if snap_name in line:166 set_conf('http_bind_address', url.netloc)
112 # Second item in list is Version167 if conf['rest_transport_uri']:
113 snap_version = line.split()[1]168 set_conf('http_publish_uri', validate_api_uri(conf['rest_transport_uri']))
114 hookenv.application_version_set(snap_version)169 if conf['web_endpoint_uri']:
170 set_conf('http_external_uri', validate_api_uri(conf['rest_transport_uri']))
115171
116 if conf.get('logextraction-rules'):172 if conf.get('logextraction-rules'):
117 rules_config = yaml.safe_load(conf.get('logextraction-rules'))173 rules_config = yaml.safe_load(conf.get('logextraction-rules'))
@@ -120,31 +176,32 @@ def configure_graylog():
120176
121 set_jvm_heap_size(conf['jvm_heap_size'])177 set_jvm_heap_size(conf['jvm_heap_size'])
122178
179 hookenv.application_version_set(snap.get_installed_version(SNAP_NAME))
180 hookenv.open_port(api_port)
181
123 remove_state('graylog_api.configured')182 remove_state('graylog_api.configured')
124 set_state('graylog.configured')183 set_state('graylog.configured')
125 report_status()184 report_status()
126185
127186
128def _validate_api_uri(uri):
129 if not uri.endswith('/'):
130 uri += '/'
131 if not uri.endswith('/api/'):
132 uri += 'api/'
133 return uri
134
135
136@when('leadership.is_leader')187@when('leadership.is_leader')
137@when_not('leadership.set.admin_password')188@when_not_all('leadership.set.admin_password', 'leadership.set.password_secret')
138def generate_admin_password():189def generate_leader_passwords():
139 admin_password = host.pwgen(18)190 if not is_state('leadership.set.admin_password'):
140 charms.leadership.leader_set(admin_password=admin_password)191 admin_password = host.pwgen(18)
192 charms.leadership.leader_set(admin_password=admin_password)
193 if not is_state('leadership.set.password_secret'):
194 password_secret = host.pwgen(96)
195 charms.leadership.leader_set(password_secret=password_secret)
141196
142197
143@when('leadership.changed.admin_password')198@when_any('leadership.changed.admin_password', 'leadership.changed.password_secret')
144def get_leader_admin_password():199def get_leader_passwords():
145 db = unitdata.kv()200 db = unitdata.kv()
146 admin_password = charms.leadership.leader_get('admin_password')201 admin_password = charms.leadership.leader_get('admin_password')
147 db.set('admin_password', admin_password)202 db.set('admin_password', admin_password)
203 password_secret = charms.leadership.leader_get('password_secret')
204 db.set('password_secret', password_secret)
148 remove_state('graylog.configured')205 remove_state('graylog.configured')
149206
150207
@@ -226,17 +283,21 @@ def report_status():
226@when_not('graylog.needs_restart')283@when_not('graylog.needs_restart')
227@when_not('graylog_api.configured')284@when_not('graylog_api.configured')
228def configure_graylog_api(*discard):285def configure_graylog_api(*discard):
286 """Adjust states to ensure API dependents will be correctly configured."""
287 remove_state('beat.setup')
229 remove_state('graylog_index_sets.configured')288 remove_state('graylog_index_sets.configured')
230 remove_state('graylog_inputs.configured')289 remove_state('graylog_inputs.configured')
290 remove_state('graylog_nagios.configured')
231 set_state('graylog_api.configured')291 set_state('graylog_api.configured')
232292
233293
234@when('graylog_api.configured') # noqa: C901294@when('graylog.configured') # noqa: C901
295@when('graylog_api.configured')
235@when_not('graylog_index_sets.configured')296@when_not('graylog_index_sets.configured')
236def configure_index_sets(*discard):297def configure_index_sets(*discard):
237 conf = hookenv.config()298 conf = hookenv.config()
238 db = unitdata.kv()299 db = unitdata.kv()
239 g = GraylogApi(base_url=API_URL,300 g = GraylogApi(base_url=get_api_url(),
240 username='admin',301 username='admin',
241 password=db.get('admin_password'),302 password=db.get('admin_password'),
242 token_name='graylog-charm')303 token_name='graylog-charm')
@@ -289,11 +350,11 @@ def configure_index_sets(*discard):
289350
290def _configure_logextraction(rules_config):351def _configure_logextraction(rules_config):
291 db = unitdata.kv()352 db = unitdata.kv()
292 g = GraylogApi(base_url=API_URL,353 g = GraylogApi(base_url=get_api_url(),
293 username='admin',354 username='admin',
294 password=db.get('admin_password'),355 password=db.get('admin_password'),
295 token_name='graylog-charm')356 token_name='graylog-charm')
296 logext = logextract.LogExtractPipeline(g)357 logext = LogExtractPipeline(g)
297 logext.set_rules(rules_config)358 logext.set_rules(rules_config)
298359
299360
@@ -344,7 +405,7 @@ def _remove_old_inputs(inputs, new_inputs):
344 UI.405 UI.
345 """406 """
346 db = unitdata.kv()407 db = unitdata.kv()
347 g = GraylogApi(base_url=API_URL,408 g = GraylogApi(base_url=get_api_url(),
348 username='admin',409 username='admin',
349 password=db.get('admin_password'),410 password=db.get('admin_password'),
350 token_name='graylog-charm')411 token_name='graylog-charm')
@@ -373,8 +434,7 @@ def graylog_remove_filebeat_input():
373 hookenv.log("Removing filebeats input")434 hookenv.log("Removing filebeats input")
374435
375 db = unitdata.kv()436 db = unitdata.kv()
376437 g = GraylogApi(base_url=get_api_url(),
377 g = GraylogApi(base_url=API_URL,
378 username='admin',438 username='admin',
379 password=db.get('admin_password'),439 password=db.get('admin_password'),
380 token_name='graylog-charm')440 token_name='graylog-charm')
@@ -393,22 +453,17 @@ def graylog_remove_filebeat_input():
393 remove_state('beat.setup')453 remove_state('beat.setup')
394454
395455
456@when('graylog.configured')
457@when('graylog_api.configured')
396@when('beats.connected')458@when('beats.connected')
397def provide_beats_port(filebeat):459@when_not('beat.setup')
398 beats_port = hookenv.config('beats_port')460def setup_beats_input(filebeat):
399 filebeat.provide_data(beats_port)
400 set_state('beat.setup')
401
402
403@when('beat.setup')
404def setup_beats_input():
405
406 port = hookenv.config('beats_port')461 port = hookenv.config('beats_port')
407 hookenv.log("Setting up beats input on port {}".format(port))462 hookenv.log("Setting up beats input on port {}".format(port))
463 filebeat.provide_data(port)
408464
409 db = unitdata.kv()465 db = unitdata.kv()
410466 g = GraylogApi(base_url=get_api_url(),
411 g = GraylogApi(base_url=API_URL,
412 username='admin',467 username='admin',
413 password=db.get('admin_password'),468 password=db.get('admin_password'),
414 token_name='graylog-charm')469 token_name='graylog-charm')
@@ -444,7 +499,10 @@ def setup_beats_input():
444 else:499 else:
445 hookenv.log("beats input already configured")500 hookenv.log("beats input already configured")
446501
502 set_state('beat.setup')
503
447504
505@when('graylog.configured')
448@when('graylog_api.configured')506@when('graylog_api.configured')
449@when_not('graylog_inputs.configured')507@when_not('graylog_inputs.configured')
450def configure_inputs(*discard):508def configure_inputs(*discard):
@@ -453,7 +511,7 @@ def configure_inputs(*discard):
453 """511 """
454 conf = hookenv.config()512 conf = hookenv.config()
455 db = unitdata.kv()513 db = unitdata.kv()
456 g = GraylogApi(base_url=API_URL,514 g = GraylogApi(base_url=get_api_url(),
457 username='admin',515 username='admin',
458 password=db.get('admin_password'),516 password=db.get('admin_password'),
459 token_name='graylog-charm')517 token_name='graylog-charm')
@@ -569,8 +627,9 @@ def update_reverseproxy_config(website):
569 url = urlparse(conf['web_listen_uri'])627 url = urlparse(conf['web_listen_uri'])
570 website.configure(port=url.port)628 website.configure(port=url.port)
571 services += "- {service_name: web, service_port: " + str(url.port) + "}\n"629 services += "- {service_name: web, service_port: " + str(url.port) + "}\n"
572630 if is_v2():
573 services += "- {service_name: api, service_port: " + API_PORT + "}\n"631 # NB: there is no revproxy to the api endpoint in v3+.
632 services += "- {service_name: api, service_port: " + get_api_port() + "}\n"
574 website.set_remote(all_services=services)633 website.set_remote(all_services=services)
575634
576635
@@ -692,16 +751,17 @@ def set_jvm_heap_size(heap_size='1G', conf_path=SERVER_DEFAULT_CONF_FILE):
692751
693@when('graylog.configured')752@when('graylog.configured')
694@when('nrpe-external-master.available')753@when('nrpe-external-master.available')
754@when_not('graylog_nagios.configured')
695def configure_nagios(nagios):755def configure_nagios(nagios):
696 if hookenv.hook_name() == 'update-status':
697 return
698
699 db = unitdata.kv()756 db = unitdata.kv()
700 nagios_password = db.get('nagios_password')757 nagios_password = db.get('nagios_password')
701 if not nagios_password:758 if not nagios_password:
702 nagios_password = host.pwgen(18)759 nagios_password = host.pwgen(18)
703 db.set('nagios_password', nagios_password)760 db.set('nagios_password', nagios_password)
704761
762 api_port = get_api_port()
763 api_url = get_api_url()
764
705 # Ask charmhelpers.contrib.charmsupport's nrpe to work out our hostname765 # Ask charmhelpers.contrib.charmsupport's nrpe to work out our hostname
706 hostname = nrpe.get_nagios_hostname()766 hostname = nrpe.get_nagios_hostname()
707 nrpe_setup = nrpe.NRPE(hostname=hostname, primary=True)767 nrpe_setup = nrpe.NRPE(hostname=hostname, primary=True)
@@ -728,11 +788,11 @@ def configure_nagios(nagios):
728 nrpe_setup.add_check(788 nrpe_setup.add_check(
729 'graylog_api',789 'graylog_api',
730 'Greylog API check',790 'Greylog API check',
731 '/usr/lib/nagios/plugins/check_http -I 127.0.0.1 -p {} -u /api -s "{}"'.format(API_PORT, check_string)791 '/usr/lib/nagios/plugins/check_http -I 127.0.0.1 -p {} -u /api -s "{}"'.format(api_port, check_string)
732 )792 )
733793
734 # For nagios checks via API, we need to create a user and token794 # For nagios checks via API, we need to create a user and token
735 g = GraylogApi(base_url=API_URL,795 g = GraylogApi(base_url=api_url,
736 username='admin',796 username='admin',
737 password=db.get('admin_password'),797 password=db.get('admin_password'),
738 token_name='graylog-charm')798 token_name='graylog-charm')
@@ -740,7 +800,7 @@ def configure_nagios(nagios):
740 g.user_permissions_set('nagios', ['users:tokenlist', 'users:tokencreate',800 g.user_permissions_set('nagios', ['users:tokenlist', 'users:tokencreate',
741 'indexercluster:read', 'indices:failures',801 'indexercluster:read', 'indices:failures',
742 'notifications:read', 'journal:read'])802 'notifications:read', 'journal:read'])
743 g = GraylogApi(base_url=API_URL,803 g = GraylogApi(base_url=api_url,
744 username='nagios',804 username='nagios',
745 password=nagios_password,805 password=nagios_password,
746 token_name='nagios')806 token_name='nagios')
@@ -764,7 +824,8 @@ def configure_nagios(nagios):
764 os.path.join(nagios_plugins, 'check_graylog_health.py'),824 os.path.join(nagios_plugins, 'check_graylog_health.py'),
765 conf.get('nagios_uncommitted_warn', 0),825 conf.get('nagios_uncommitted_warn', 0),
766 conf.get('nagios_uncommitted_crit', 100),826 conf.get('nagios_uncommitted_crit', 100),
767 API_URL)827 api_url)
768 )828 )
769829
770 nrpe_setup.write()830 nrpe_setup.write()
831 set_state('graylog_nagios.configured')
diff --git a/unit_tests/test_graylog.py b/unit_tests/test_graylog.py
index 38ac6fa..37fb78d 100644
--- a/unit_tests/test_graylog.py
+++ b/unit_tests/test_graylog.py
@@ -3,20 +3,169 @@ import sys
3import tempfile3import tempfile
4import unittest4import unittest
5from unittest import mock5from unittest import mock
6from charms.reactive import set_state, is_state
67
7from charms.layer.graylog.api import GraylogApi8# some dep layers only exists in the built charm; mock them out before
89# the graylog imports since those depend on post-build charms.* layers
9# charms.leadership only exists in the built charm; mock it out before10leader_mock = mock.Mock()
10# the graylog imports since those depend on charms.leadership11sys.modules['charms.leadership'] = leader_mock
11layer_mock = mock.Mock()12snap_mock = mock.Mock()
12sys.modules['charms.leadership'] = layer_mock13sys.modules['charms.layer.snap'] = snap_mock
1314
14from reactive.graylog import (15from reactive.graylog import (
16 GraylogApi,
17 configure_graylog,
18 install_graylog,
19 refresh_graylog,
15 set_conf,20 set_conf,
16 set_jvm_heap_size,21 set_jvm_heap_size,
22 update_config,
23 upgrade_charm,
17 _check_input_exists) # noqa: E40224 _check_input_exists) # noqa: E402
18from files import check_graylog_health # noqa: E40225from files import check_graylog_health # noqa: E402
1926
27
28class TestInstallUpdateUpgrade(unittest.TestCase):
29 """Test that install / update / upgrade clears our configured flag."""
30
31 @mock.patch('reactive.graylog.snap')
32 @mock.patch('reactive.graylog.hookenv.config')
33 def test_install(self, mock_config, mock_snap):
34 class Config(dict):
35 def __init__(self, *args, **kw):
36 super(Config, self).__init__(*args, **kw)
37
38 mock_config.return_value = Config({'channel': '2/stable'})
39 install_graylog()
40 self.assertTrue(mock_snap.install.called)
41 self.assertFalse(is_state('graylog.configured'))
42
43 def test_update(self):
44 set_state('graylog.configured')
45 update_config()
46 self.assertFalse(is_state('graylog.configured'))
47
48 def test_upgrade(self):
49 set_state('graylog.configured')
50 upgrade_charm()
51 self.assertFalse(is_state('graylog.configured'))
52
53
54class TestRefresh(unittest.TestCase):
55 """Test the snap refresh handler."""
56
57 @mock.patch('reactive.graylog.snap')
58 @mock.patch('reactive.graylog.hookenv.config')
59 def test_no_channel(self, mock_config, mock_snap):
60 """Ensure a no-op if there is no 'channel' config."""
61 mock_config.return_value = ''
62 mock_snap.get_installed_version.return_value = '2.foo'
63 set_state('graylog.configured')
64 refresh_graylog()
65 self.assertFalse(mock_snap.refresh.called)
66 self.assertTrue(is_state('graylog.configured'))
67
68 @mock.patch('reactive.graylog.shutil')
69 @mock.patch('reactive.graylog.os')
70 @mock.patch('reactive.graylog.host')
71 @mock.patch('reactive.graylog.snap')
72 @mock.patch('reactive.graylog.hookenv.config')
73 def test_no_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil):
74 """Ensure no reconfiguration if the snap version does not change."""
75 mock_config.return_value = 'foo'
76 mock_snap.get_installed_version.side_effect = ['2.foo', '2.foo']
77 mock_os.path.exists.return_value = True
78 mock_shutil.copy2.return_value = 'foo'
79 set_state('graylog.configured')
80 refresh_graylog()
81 self.assertTrue(mock_snap.refresh.called)
82 self.assertTrue(is_state('graylog.configured'))
83
84 @mock.patch('reactive.graylog.data_changed')
85 @mock.patch('reactive.graylog.shutil.copy2')
86 @mock.patch('reactive.graylog.os')
87 @mock.patch('reactive.graylog.host')
88 @mock.patch('reactive.graylog.snap')
89 @mock.patch('reactive.graylog.hookenv.config')
90 def test_change(self, mock_config, mock_snap, mock_host, mock_os, mock_shutil, mock_data_changed):
91 """Ensure calls and states when the snap has changed."""
92 mock_config.return_value = 'foo'
93 mock_snap.get_installed_version.side_effect = ['2.foo', '3.foo']
94 mock_os.path.exists.return_value = True
95 mock_shutil.return_value = 'foo'
96 set_state('graylog.configured')
97 refresh_graylog()
98 self.assertTrue(mock_host.service_stop.called)
99 self.assertTrue(mock_snap.refresh.called)
100 self.assertEqual(mock_shutil.call_count, 2)
101 self.assertEqual(mock_data_changed.call_count, 2)
102 self.assertFalse(is_state('graylog.configured'))
103
104
105class TestConfig(unittest.TestCase):
106 """Test the configure_graylog handler."""
107
108 @mock.patch('reactive.graylog.hookenv')
109 @mock.patch('reactive.graylog.os')
110 def test_no_conf_file(self, mock_os, mock_hookenv):
111 """Ensure a status message if there is no config file present."""
112 mock_os.path.exists.return_value = False
113 configure_graylog()
114 self.assertTrue(mock_hookenv.status_set.called)
115
116 @mock.patch('reactive.graylog.set_jvm_heap_size')
117 @mock.patch('reactive.graylog.validate_api_uri')
118 @mock.patch('reactive.graylog.is_v2')
119 @mock.patch('reactive.graylog.set_conf')
120 @mock.patch('reactive.graylog.hookenv')
121 @mock.patch('reactive.graylog.os')
122 @mock.patch('reactive.graylog.unitdata')
123 def test_v2_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, mock_validate_uri, mock_jvm_heap):
124 """Ensure calls and states with various config values for v2."""
125 class Config(dict):
126 def __init__(self, *args, **kw):
127 super(Config, self).__init__(*args, **kw)
128
129 mock_ud.kv.return_value = {'admin_password': 'password', 'password_secret': 'secret'}
130 mock_os.path.exists.return_value = True
131 mock_hookenv.config.return_value = Config({'jvm_heap_size': '1G',
132 'web_listen_uri': None,
133 'rest_transport_uri': 'foo/api',
134 'web_endpoint_uri': 'bar/api'})
135 mock_v2.return_value = True
136 configure_graylog()
137 self.assertEqual(mock_validate_uri.call_count, 2)
138 self.assertTrue(mock_jvm_heap.called)
139 self.assertTrue(mock_hookenv.application_version_set.called)
140 self.assertTrue(mock_hookenv.open_port.called)
141
142 @mock.patch('reactive.graylog.set_jvm_heap_size')
143 @mock.patch('reactive.graylog.validate_api_uri')
144 @mock.patch('reactive.graylog.is_v2')
145 @mock.patch('reactive.graylog.set_conf')
146 @mock.patch('reactive.graylog.hookenv')
147 @mock.patch('reactive.graylog.os')
148 @mock.patch('reactive.graylog.unitdata')
149 def test_v3_config(self, mock_ud, mock_os, mock_hookenv, mock_set_conf, mock_v2, mock_validate_uri, mock_jvm_heap):
150 """Ensure calls and states with various config values for v3."""
151 class Config(dict):
152 def __init__(self, *args, **kw):
153 super(Config, self).__init__(*args, **kw)
154
155 mock_ud.kv.return_value = {'admin_password': 'password', 'password_secret': 'secret'}
156 mock_os.path.exists.return_value = True
157 mock_hookenv.config.return_value = Config({'jvm_heap_size': '1G',
158 'web_listen_uri': None,
159 'rest_transport_uri': 'foo/api',
160 'web_endpoint_uri': 'bar/api'})
161 mock_v2.return_value = False
162 configure_graylog()
163 self.assertEqual(mock_validate_uri.call_count, 2)
164 self.assertTrue(mock_jvm_heap.called)
165 self.assertTrue(mock_hookenv.application_version_set.called)
166 self.assertTrue(mock_hookenv.open_port.called)
167
168
20initial_conf = u"""#key1 = value1169initial_conf = u"""#key1 = value1
21key2 = value2170key2 = value2
22key2=value3171key2=value3
diff --git a/unit_tests/test_lib.py b/unit_tests/test_lib.py
23new file mode 100644172new file mode 100644
index 0000000..565ffc1
--- /dev/null
+++ b/unit_tests/test_lib.py
@@ -0,0 +1,36 @@
1import unittest
2from unittest import mock
3
4from charms.layer.graylog import (
5 get_api_port,
6 get_api_url,
7 validate_api_uri,
8)
9
10
11class TestLibraryUtils(unittest.TestCase):
12 """Test lib.charms.layer.graylog utils."""
13
14 @mock.patch('charms.layer.graylog.snap.get_installed_version')
15 def test_api(self, mock_snap):
16 """Validate the api port/url match what we expect for v2 and v3."""
17 mock_snap.return_value = '2.foo'
18 self.assertEqual(get_api_port(), '9001')
19 self.assertEqual(get_api_url(), 'http://127.0.0.1:9001/api/')
20
21 mock_snap.return_value = '3.bar'
22 self.assertEqual(get_api_port(), '9000')
23 self.assertEqual(get_api_url(), 'http://127.0.0.1:9000/api/')
24
25 @mock.patch('charms.layer.graylog.snap.get_installed_version')
26 def test_validate_api_url(self, mock_snap):
27 """Validate the URI suffix is valid for v2 and v3."""
28 mock_snap.return_value = '2.foo'
29 self.assertEqual(validate_api_uri('foo'), 'foo/api/')
30 self.assertEqual(validate_api_uri('foo/'), 'foo/api/')
31 self.assertEqual(validate_api_uri('foo/api'), 'foo/api/')
32
33 mock_snap.return_value = '3.bar'
34 self.assertEqual(validate_api_uri('foo'), 'foo/')
35 self.assertEqual(validate_api_uri('foo/'), 'foo/')
36 self.assertEqual(validate_api_uri('foo/api/'), 'foo/')
diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py
index d3ea072..5bae816 100644
--- a/unit_tests/test_logextract.py
+++ b/unit_tests/test_logextract.py
@@ -1,7 +1,7 @@
1import unittest1import unittest
2from unittest import mock2from unittest import mock
33
4from charms.layer.graylog import logextract4from charms.layer.graylog import GraylogPipelines, GraylogRules, GraylogStreams, LogExtractPipeline
55
66
7testdata_simple_pipeline_source = '\npipeline "testpipe"\nstage 1 match either\n rule "test-rule";\nend\n'7testdata_simple_pipeline_source = '\npipeline "testpipe"\nstage 1 match either\n rule "test-rule";\nend\n'
@@ -47,14 +47,14 @@ class TestGraylogPipelines(unittest.TestCase):
47 @mock.patch('charms.layer.graylog.api.GraylogApi')47 @mock.patch('charms.layer.graylog.api.GraylogApi')
48 def test_get_pipeline_for(self, mock_api):48 def test_get_pipeline_for(self, mock_api):
49 mock_api.request.return_value = testdata_simple_pipeline49 mock_api.request.return_value = testdata_simple_pipeline
50 gpipes = logextract.GraylogPipelines(mock_api)50 gpipes = GraylogPipelines(mock_api)
51 test = gpipes.get_for_title('juju-logextract-pipeline')51 test = gpipes.get_for_title('juju-logextract-pipeline')
52 self.assertEqual(test['title'], 'juju-logextract-pipeline')52 self.assertEqual(test['title'], 'juju-logextract-pipeline')
5353
54 @mock.patch('charms.layer.graylog.api.GraylogApi')54 @mock.patch('charms.layer.graylog.api.GraylogApi')
55 def test_set_pipeline(self, mock_api):55 def test_set_pipeline(self, mock_api):
56 mock_api.request.return_value = testdata_simple_pipeline56 mock_api.request.return_value = testdata_simple_pipeline
57 gpipes = logextract.GraylogPipelines(mock_api)57 gpipes = GraylogPipelines(mock_api)
58 gpipes.set('juju-logextract-pipeline', testdata_simple_pipeline_source)58 gpipes.set('juju-logextract-pipeline', testdata_simple_pipeline_source)
59 self.assertEqual(mock_api.request.call_args_list[1][1]['data']['title'], 'juju-logextract-pipeline')59 self.assertEqual(mock_api.request.call_args_list[1][1]['data']['title'], 'juju-logextract-pipeline')
6060
@@ -64,7 +64,7 @@ class TestGraylogRules(unittest.TestCase):
64 @mock.patch('charms.layer.graylog.api.GraylogApi')64 @mock.patch('charms.layer.graylog.api.GraylogApi')
65 def test_get_rules(self, mock_api):65 def test_get_rules(self, mock_api):
66 mock_api.request.return_value = testdata_rules66 mock_api.request.return_value = testdata_rules
67 grules = logextract.GraylogRules(mock_api)67 grules = GraylogRules(mock_api)
68 rules = grules.get()68 rules = grules.get()
69 self.assertEqual(rules[0]['title'], "from ceph mon")69 self.assertEqual(rules[0]['title'], "from ceph mon")
7070
@@ -74,7 +74,7 @@ class TestGraylogStreams(unittest.TestCase):
74 @mock.patch('charms.layer.graylog.api.GraylogApi')74 @mock.patch('charms.layer.graylog.api.GraylogApi')
75 def test_get_stream_id_for(self, mock_api):75 def test_get_stream_id_for(self, mock_api):
76 mock_api.request.return_value = testdata_streams76 mock_api.request.return_value = testdata_streams
77 gstreams = logextract.GraylogStreams(mock_api)77 gstreams = GraylogStreams(mock_api)
78 allmsgs = gstreams.get_stream_id_for("All messages")78 allmsgs = gstreams.get_stream_id_for("All messages")
79 self.assertEqual(allmsgs, testdata_streams['streams'][0]['id'])79 self.assertEqual(allmsgs, testdata_streams['streams'][0]['id'])
80 nope = gstreams.get_stream_id_for("I don't exist")80 nope = gstreams.get_stream_id_for("I don't exist")
@@ -88,13 +88,13 @@ class TestLogExtractPipeline(unittest.TestCase):
88 mock_api.request.return_value = testdata_simple_pipeline88 mock_api.request.return_value = testdata_simple_pipeline
8989
90 def test_parse_rule_title(self):90 def test_parse_rule_title(self):
91 logext = logextract.LogExtractPipeline()91 logext = LogExtractPipeline()
92 t = logext.parse_rule_title(testdata_rules[0]['source'])92 t = logext.parse_rule_title(testdata_rules[0]['source'])
93 self.assertEqual(t, 'from ceph mon')93 self.assertEqual(t, 'from ceph mon')
9494
95 @mock.patch('charms.layer.graylog.api.GraylogApi')95 @mock.patch('charms.layer.graylog.api.GraylogApi')
96 def test_set_rules(self, mock_api):96 def test_set_rules(self, mock_api):
97 logext = logextract.LogExtractPipeline(mock_api)97 logext = LogExtractPipeline(mock_api)
98 logext.set_rules([testdata_rules[0]['source']])98 logext.set_rules([testdata_rules[0]['source']])
99 exp = [mock.call('/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline')]99 exp = [mock.call('/plugins/org.graylog.plugins.pipelineprocessor/system/pipelines/pipeline')]
100 mock_api.request.assert_has_calls(exp)100 mock_api.request.assert_has_calls(exp)
diff --git a/wheelhouse.txt b/wheelhouse.txt
101new file mode 100644101new file mode 100644
index 0000000..d555101
--- /dev/null
+++ b/wheelhouse.txt
@@ -0,0 +1 @@
1requests>=2.0.0,<3.0.0

Subscribers

People subscribed via source and target branches

to all changes: