Merge ~graylog-charmers/charm-graylog:feature/multi-version into ~graylog-charmers/charm-graylog:master
- Git
- lp:~graylog-charmers/charm-graylog
- feature/multi-version
- Merge into master
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) |
Related bugs: |
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-
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
🤖 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.
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_
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:
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:/
(Note the above is from a quick-and-dirty, backwards-
Paul Goins (vultaire) wrote : | # |
Withdrew one of my comments.
Rodrigo Barbieri (rodrigo-barbieri2010) wrote : | # |
Updated with the review feedback of Paul Groins.
Changed
@when_not(
def install_graylog():
to
@when('
def install_graylog():
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.
Rodrigo Barbieri (rodrigo-barbieri2010) wrote : | # |
Thanks again Paul. I updated the config option description.
Paul Goins (vultaire) : | # |
Kevin W Monroe (kwmonroe) wrote : | # |
@when('
def install_graylog():
Can't do that ^^. Reactive will run 'install_graylog' every 5 minutes (every update_status hook) if we do.
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/
> b/lib/charms/
> > index e69de29..38bffbe 100644
> > --- a/lib/charms/
> > +++ b/lib/charms/
> > @@ -0,0 +1,39 @@
> > +from charmhelpers.
>
> 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/
> > index 0d64f26..4a718bd 100644
> > --- a/reactive/
> > +++ b/reactive/
> > @@ -55,15 +67,56 @@ rotation_strategies = {
> > }
> >
> >
> > +@when_
>
> 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('
> 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('
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(
regardless of prereqs like 'core' being installed. In fact, layer-snap's
install function will set 'snap.installed
https:/
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.
Paul Goins (vultaire) wrote : | # |
The above being said, the intent of my review comment wasn't to remove "@when_
@when(
@when_
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?
Kevin W Monroe (kwmonroe) wrote : | # |
I addressed previous comments in this last round of commits. Specifically:
- Guard install_graylog with @when_not(
- 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/
- 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-
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-
Kevin W Monroe (kwmonroe) wrote : | # |
IS deploy/upgrade tests were successful:
https:/
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/
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change has no commit message, setting status to needs review.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 7cb4b82ab266da4
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 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. |
132 | diff --git a/config.yaml b/config.yaml |
133 | index 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 |
198 | diff --git a/layer.yaml b/layer.yaml |
199 | index 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 |
220 | diff --git a/lib/charms/layer/graylog/__init__.py b/lib/charms/layer/graylog/__init__.py |
221 | index 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 |
228 | diff --git a/lib/charms/layer/graylog/logextract.py b/lib/charms/layer/graylog/logextract.py |
229 | index 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) |
267 | diff --git a/lib/charms/layer/graylog/utils.py b/lib/charms/layer/graylog/utils.py |
268 | new file mode 100644 |
269 | index 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 |
315 | diff --git a/reactive/graylog.py b/reactive/graylog.py |
316 | index 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') |
739 | diff --git a/unit_tests/test_graylog.py b/unit_tests/test_graylog.py |
740 | index 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 |
919 | diff --git a/unit_tests/test_lib.py b/unit_tests/test_lib.py |
920 | new file mode 100644 |
921 | index 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/') |
961 | diff --git a/unit_tests/test_logextract.py b/unit_tests/test_logextract.py |
962 | index 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) |
1025 | diff --git a/wheelhouse.txt b/wheelhouse.txt |
1026 | new file mode 100644 |
1027 | index 0000000..d555101 |
1028 | --- /dev/null |
1029 | +++ b/wheelhouse.txt |
1030 | @@ -0,0 +1 @@ |
1031 | +requests>=2.0.0,<3.0.0 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.