Merge ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator into charm-k8s-wordpress:master

Proposed by Barry Price
Status: Merged
Merged at revision: 5d73401594539e7abca5cfebbd38a1e9e2883c4a
Proposed branch: ~barryprice/charm-k8s-wordpress/+git/charm-k8s-wordpress:operator
Merge into: charm-k8s-wordpress:master
Diff against target: 778 lines (+307/-78)
10 files modified
.gitignore (+1/-0)
.gitmodules (+3/-0)
Makefile (+2/-9)
dev/null (+0/-1)
hooks/start (+1/-0)
lib/ops (+1/-0)
mod/operator (+1/-0)
src/charm.py (+294/-0)
tests/unit/test_wordpress.py (+0/-64)
tox.ini (+4/-4)
Reviewer Review Type Date Requested Status
Wordpress Charmers Pending
Review via email: mp+381231@code.launchpad.net

Commit message

Porting to Operator (WIP)

To post a comment you must log in.
Revision history for this message
Tom Haddon (mthaddon) wrote :

Some comments inline

Revision history for this message
Barry Price (barryprice) wrote :

Thanks, left some responses and will push fixes for these as well as others.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..172bf57
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1 @@
7+.tox
8diff --git a/.gitmodules b/.gitmodules
9new file mode 100644
10index 0000000..8c05fa9
11--- /dev/null
12+++ b/.gitmodules
13@@ -0,0 +1,3 @@
14+[submodule "mod/operator"]
15+ path = mod/operator
16+ url = https://github.com/canonical/operator
17diff --git a/Makefile b/Makefile
18index a46c2e2..cf18918 100644
19--- a/Makefile
20+++ b/Makefile
21@@ -9,15 +9,8 @@ unittest:
22
23 test: lint unittest
24
25-build: lint
26- charm build
27-
28 clean:
29 @echo "Cleaning files"
30- @rm -rf ./.tox
31- @rm -rf ./.pytest_cache
32- @rm -rf ./tests/unit/__pycache__ ./reactive/__pycache__ ./lib/__pycache__
33- @rm -rf ./.coverage ./.unit-state.db
34-
35+ @git clean -fXd
36
37-.PHONY: lint test unittest build clean
38+.PHONY: lint test unittest clean
39diff --git a/hooks/start b/hooks/start
40new file mode 120000
41index 0000000..25b1f68
42--- /dev/null
43+++ b/hooks/start
44@@ -0,0 +1 @@
45+../src/charm.py
46\ No newline at end of file
47diff --git a/layer.yaml b/layer.yaml
48deleted file mode 100644
49index d7700f7..0000000
50--- a/layer.yaml
51+++ /dev/null
52@@ -1,4 +0,0 @@
53-includes:
54- - 'layer:caas-base'
55- - 'layer:status'
56-repo: git+ssh://git.launchpad.net/charm-k8s-wordpress
57diff --git a/lib/ops b/lib/ops
58new file mode 120000
59index 0000000..d934193
60--- /dev/null
61+++ b/lib/ops
62@@ -0,0 +1 @@
63+../mod/operator/ops
64\ No newline at end of file
65diff --git a/mod/operator b/mod/operator
66new file mode 160000
67index 0000000..44dff93
68--- /dev/null
69+++ b/mod/operator
70@@ -0,0 +1 @@
71+Subproject commit 44dff930667aa8e9b179c11fa87ceb8c9b85ec5a
72diff --git a/reactive/wordpress.py b/reactive/wordpress.py
73deleted file mode 100644
74index 9a1b013..0000000
75--- a/reactive/wordpress.py
76+++ /dev/null
77@@ -1,292 +0,0 @@
78-import io
79-import os
80-import re
81-import requests
82-from pprint import pprint
83-from urllib.parse import urlparse, urlunparse
84-from yaml import safe_load
85-
86-from charmhelpers.core import host, hookenv
87-from charms import reactive
88-from charms.layer import caas_base, status
89-from charms.reactive import hook, when, when_not
90-
91-
92-@hook("upgrade-charm")
93-def upgrade_charm():
94- status.maintenance("Upgrading charm")
95- reactive.clear_flag("wordpress.configured")
96-
97-
98-@when("config.changed")
99-def reconfig():
100- status.maintenance("charm configuration changed")
101- reactive.clear_flag("wordpress.configured")
102-
103- # Validate config
104- valid = True
105- config = hookenv.config()
106- # Ensure required strings
107- for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
108- if config[k].strip() == "":
109- status.blocked("{!r} config is required".format(k))
110- valid = False
111-
112- reactive.toggle_flag("wordpress.config.valid", valid)
113-
114-
115-@when("wordpress.config.valid")
116-@when_not("wordpress.configured")
117-def deploy_container():
118- spec = make_pod_spec()
119- if spec is None:
120- return # Status already set
121- if reactive.data_changed("wordpress.spec", spec):
122- status.maintenance("configuring container")
123- try:
124- caas_base.pod_spec_set(spec)
125- except Exception as e:
126- hookenv.log("pod_spec_set failed: {}".format(e), hookenv.DEBUG)
127- status.blocked("pod_spec_set failed! Check logs and k8s dashboard.")
128- return
129- else:
130- hookenv.log("No changes to pod spec")
131- if first_install():
132- reactive.set_flag("wordpress.configured")
133-
134-
135-@when("wordpress.configured")
136-def ready():
137- status.active("Ready")
138-
139-
140-def sanitized_container_config():
141- """Container config without secrets"""
142- config = hookenv.config()
143- if config["container_config"].strip() == "":
144- container_config = {}
145- else:
146- container_config = safe_load(config["container_config"])
147- if not isinstance(container_config, dict):
148- status.blocked("container_config is not a YAML mapping")
149- return None
150- container_config["WORDPRESS_DB_HOST"] = config["db_host"]
151- container_config["WORDPRESS_DB_NAME"] = config["db_name"]
152- container_config["WORDPRESS_DB_USER"] = config["db_user"]
153- if config.get("wp_plugin_openid_team_map"):
154- container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
155- return container_config
156-
157-
158-def full_container_config():
159- """Container config with secrets"""
160- config = hookenv.config()
161- container_config = sanitized_container_config()
162- if container_config is None:
163- return None
164- if config["container_secrets"].strip() == "":
165- container_secrets = {}
166- else:
167- container_secrets = safe_load(config["container_secrets"])
168- if not isinstance(container_secrets, dict):
169- status.blocked("container_secrets is not a YAML mapping")
170- return None
171- container_config.update(container_secrets)
172- # Add secrets from charm config
173- container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
174- if config.get("wp_plugin_akismet_key"):
175- container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
176- return container_config
177-
178-
179-def make_pod_spec():
180- config = hookenv.config()
181- container_config = sanitized_container_config()
182- if container_config is None:
183- return # Status already set
184-
185- ports = [
186- {"name": name, "containerPort": int(port), "protocol": "TCP"}
187- for name, port in [addr.split(":", 1) for addr in config["ports"].split()]
188- ]
189-
190- # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
191- spec = {
192- "containers": [
193- {
194- "name": hookenv.charm_name(),
195- "imageDetails": {"imagePath": config["image"]},
196- "ports": ports,
197- "config": container_config,
198- }
199- ]
200- }
201- out = io.StringIO()
202- pprint(spec, out)
203- hookenv.log("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
204-
205- # If we need credentials (secrets) for our image, add them to the spec after logging
206- if config.get("image_user") and config.get("image_pass"):
207- spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
208- spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
209-
210- config_with_secrets = full_container_config()
211- if config_with_secrets is None:
212- return None # Status already set
213- container_config.update(config_with_secrets)
214-
215- return spec
216-
217-
218-def first_install():
219- """Perform initial configuration of wordpress if needed."""
220- config = hookenv.config()
221- if not is_pod_up("website"):
222- hookenv.log("Pod not yet ready - retrying")
223- return False
224- elif not is_vhost_ready():
225- hookenv.log("Wordpress vhost is not yet listening - retrying")
226- return False
227- elif wordpress_configured() or not config["initial_settings"]:
228- hookenv.log("No initial_setting provided or wordpress already configured. Skipping first install.")
229- return True
230- hookenv.log("Starting wordpress initial configuration")
231- payload = {
232- "admin_password": host.pwgen(24),
233- "blog_public": "checked",
234- "Submit": "submit",
235- }
236- payload.update(safe_load(config["initial_settings"]))
237- payload["admin_password2"] = payload["admin_password"]
238- if not payload["blog_public"]:
239- payload["blog_public"] = "unchecked"
240- required_config = set(("user_name", "admin_email"))
241- missing = required_config.difference(payload.keys())
242- if missing:
243- hookenv.log("Error: missing wordpress settings: {}".format(missing))
244- return False
245- call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
246- host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
247- return True
248-
249-
250-def call_wordpress(uri, redirects=True, payload={}, _depth=1):
251- max_depth = 10
252- if _depth > max_depth:
253- hookenv.log("Redirect loop detected in call_worpress()")
254- raise RuntimeError("Redirect loop detected in call_worpress()")
255- config = hookenv.config()
256- service_ip = get_service_ip("website")
257- if service_ip:
258- headers = {"Host": config["blog_hostname"]}
259- url = urlunparse(("http", service_ip, uri, "", "", ""))
260- if payload:
261- r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
262- else:
263- r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
264- if redirects and r.is_redirect:
265- # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
266- o = urlparse(r.headers.get("Location"))
267- return call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
268- else:
269- return r
270- else:
271- hookenv.log("Error getting service IP")
272- return False
273-
274-
275-def wordpress_configured():
276- """Check whether first install has been completed."""
277- # Check whether pod is deployed
278- if not is_pod_up("website"):
279- return False
280- # Check if we have WP code deployed at all
281- if not is_vhost_ready():
282- return False
283- # We have code on disk, check if configured
284- try:
285- r = call_wordpress("/", redirects=False)
286- except requests.exceptions.ConnectionError:
287- return False
288- if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")):
289- return False
290- elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
291- hookenv.log("MySQL database setup failed, we likely have no wp-config.php")
292- status.blocked("MySQL database setup failed, we likely have no wp-config.php")
293- return False
294- else:
295- return True
296-
297-
298-def is_vhost_ready():
299- """Check whether wordpress is available using http."""
300- # Check if we have WP code deployed at all
301- try:
302- r = call_wordpress("/wp-login.php", redirects=False)
303- except requests.exceptions.ConnectionError:
304- hookenv.log("call_wordpress() returned requests.exceptions.ConnectionError")
305- return False
306- if r is None:
307- hookenv.log("call_wordpress() returned None")
308- return False
309- if hasattr(r, "status_code") and r.status_code in (403, 404):
310- hookenv.log("call_wordpress() returned status {}".format(r.status_code))
311- return False
312- else:
313- return True
314-
315-
316-def get_service_ip(endpoint):
317- try:
318- info = hookenv.network_get(endpoint, hookenv.relation_id())
319- if "ingress-addresses" in info:
320- addr = info["ingress-addresses"][0]
321- if len(addr):
322- return addr
323- else:
324- hookenv.log("No ingress-addresses: {}".format(info))
325- except Exception as e:
326- hookenv.log("Caught exception checking for service IP: {}".format(e))
327-
328- return None
329-
330-
331-def is_pod_up(endpoint):
332- """Check to see if the pod of a relation is up.
333-
334- application-vimdb: 19:29:10 INFO unit.vimdb/0.juju-log network info
335-
336- In the example below:
337- - 10.1.1.105 is the address of the application pod.
338- - 10.152.183.199 is the service cluster ip
339-
340- {
341- 'bind-addresses': [{
342- 'macaddress': '',
343- 'interfacename': '',
344- 'addresses': [{
345- 'hostname': '',
346- 'address': '10.1.1.105',
347- 'cidr': ''
348- }]
349- }],
350- 'egress-subnets': [
351- '10.152.183.199/32'
352- ],
353- 'ingress-addresses': [
354- '10.152.183.199',
355- '10.1.1.105'
356- ]
357- }
358- """
359- try:
360- info = hookenv.network_get(endpoint, hookenv.relation_id())
361-
362- # Check to see if the pod has been assigned its internal and external ips
363- for ingress in info["ingress-addresses"]:
364- if len(ingress) == 0:
365- return False
366- except Exception:
367- return False
368-
369- return True
370diff --git a/src/charm.py b/src/charm.py
371new file mode 100755
372index 0000000..a19bc28
373--- /dev/null
374+++ b/src/charm.py
375@@ -0,0 +1,294 @@
376+#!/usr/bin/env python3
377+
378+import io
379+import re
380+import subprocess
381+import sys
382+from pprint import pprint
383+from time import sleep
384+from urllib.parse import urlparse, urlunparse
385+from yaml import safe_load
386+
387+sys.path.append("lib")
388+
389+from ops.charm import CharmBase # NoQA: E402
390+from ops.framework import StoredState # NoQA: E402
391+from ops.main import main # NoQA: E402
392+from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus # NoQA: E402
393+
394+import logging # NoQA: E402
395+
396+logger = logging.getLogger()
397+
398+
399+class Wordpress(CharmBase):
400+ state = StoredState()
401+
402+ def __init__(self, *args):
403+ super().__init__(*args)
404+ for event in (
405+ self.on.start,
406+ self.on.config_changed,
407+ ):
408+ self.framework.observe(event, self)
409+
410+ def on_start(self, event):
411+ logger.info("Here we go...")
412+ # There may be a nicer way to do this!
413+ # https://github.com/canonical/operator/issues/156
414+ subprocess.check_call(['apt-get', 'update'])
415+ subprocess.check_call(['apt-get', '-y', 'install', 'python3-requests'])
416+ # Initialise states
417+ self.state._started = True
418+ self.state._configured = False
419+ self.state._valid = False
420+
421+ def on_config_changed(self, event):
422+ valid = True
423+ config = self.model.config
424+ for k in ["image", "db_host", "db_name", "db_user", "db_password"]:
425+ if config[k].strip() == "":
426+ self.model.unit.status = BlockedStatus("{!r} config is required".format(k))
427+ valid = False
428+ self.state._valid = valid
429+ self.configure_pod(event)
430+
431+ def sanitized_container_config(self):
432+ """Container config without secrets"""
433+ config = self.model.config
434+ if config["container_config"].strip() == "":
435+ container_config = {}
436+ else:
437+ container_config = safe_load(config["container_config"])
438+ if not isinstance(container_config, dict):
439+ self.model.unit.status = BlockedStatus("container_config is not a YAML mapping")
440+ return None
441+ container_config["WORDPRESS_DB_HOST"] = config["db_host"]
442+ container_config["WORDPRESS_DB_NAME"] = config["db_name"]
443+ container_config["WORDPRESS_DB_USER"] = config["db_user"]
444+ if config.get("wp_plugin_openid_team_map"):
445+ container_config["WP_PLUGIN_OPENID_TEAM_MAP"] = config["wp_plugin_openid_team_map"]
446+ return container_config
447+
448+ def full_container_config(self):
449+ """Container config with secrets"""
450+ config = self.model.config
451+ container_config = self.sanitized_container_config()
452+ if container_config is None:
453+ return None
454+ if config["container_secrets"].strip() == "":
455+ container_secrets = {}
456+ else:
457+ container_secrets = safe_load(config["container_secrets"])
458+ if not isinstance(container_secrets, dict):
459+ self.model.unit.status = BlockedStatus("container_secrets is not a YAML mapping")
460+ return None
461+ container_config.update(container_secrets)
462+ # Add secrets from charm config
463+ container_config["WORDPRESS_DB_PASSWORD"] = config["db_password"]
464+ if config.get("wp_plugin_akismet_key"):
465+ container_config["WP_PLUGIN_AKISMET_KEY"] = config["wp_plugin_akismet_key"]
466+ return container_config
467+
468+ def make_pod_spec(self):
469+ config = self.model.config
470+ container_config = self.sanitized_container_config()
471+ if container_config is None:
472+ return # Status already set
473+
474+ ports = [
475+ {"name": name, "containerPort": int(port), "protocol": "TCP"}
476+ for name, port in [addr.split(":", 1) for addr in config["ports"].split()]
477+ ]
478+
479+ # PodSpec v1? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#podspec-v1-core
480+ spec = {
481+ "containers": [
482+ {
483+ "name": self.app.name,
484+ "imageDetails": {"imagePath": config["image"]},
485+ "ports": ports,
486+ "config": container_config,
487+ "readinessProbe": {"exec": {"command": ["cat", "/srv/wordpress-helpers/.ready"]}},
488+ }
489+ ]
490+ }
491+ out = io.StringIO()
492+ pprint(spec, out)
493+ logger.info("Container environment config (sans secrets) <<EOM\n{}\nEOM".format(out.getvalue()))
494+
495+ # If we need credentials (secrets) for our image, add them to the spec after logging
496+ if config.get("image_user") and config.get("image_pass"):
497+ spec.get("containers")[0].get("imageDetails")["username"] = config["image_user"]
498+ spec.get("containers")[0].get("imageDetails")["password"] = config["image_pass"]
499+
500+ config_with_secrets = self.full_container_config()
501+ if config_with_secrets is None:
502+ return None # Status already set
503+ container_config.update(config_with_secrets)
504+
505+ return spec
506+
507+ def first_install(self):
508+ """Perform initial configuration of wordpress if needed."""
509+ config = self.model.config
510+ if not self.is_pod_up("website"):
511+ logger.info("Pod not yet ready - retrying")
512+ return False
513+ elif not self.is_vhost_ready():
514+ logger.info("Wordpress vhost is not yet listening - retrying")
515+ return False
516+ elif self.state._configured or not config["initial_settings"]:
517+ logger.info("No initial_setting provided or wordpress already configured. Skipping first install.")
518+ return True
519+ logger.info("Starting wordpress initial configuration")
520+ payload = {
521+ # "admin_password": host.pwgen(24), ## TODO: pwgen
522+ "admin_password": "letmein123",
523+ "blog_public": "checked",
524+ "Submit": "submit",
525+ }
526+ payload.update(safe_load(config["initial_settings"]))
527+ payload["admin_password2"] = payload["admin_password"]
528+ if not payload["blog_public"]:
529+ payload["blog_public"] = "unchecked"
530+ required_config = set(("user_name", "admin_email"))
531+ missing = required_config.difference(payload.keys())
532+ if missing:
533+ logger.info("Error: missing wordpress settings: {}".format(missing))
534+ return False
535+ self.call_wordpress("/wp-admin/install.php?step=2", redirects=True, payload=payload)
536+ # TODO: write_file
537+ # host.write_file(os.path.join("/root/", "initial.passwd"), payload["admin_password"], perms=0o400)
538+ return True
539+
540+ def call_wordpress(self, uri, redirects=True, payload={}, _depth=1):
541+ import requests
542+
543+ max_depth = 10
544+ if _depth > max_depth:
545+ logger.info("Redirect loop detected in call_worpress()")
546+ raise RuntimeError("Redirect loop detected in call_worpress()")
547+ config = self.model.config
548+ service_ip = self.get_service_ip("website")
549+ if service_ip:
550+ headers = {"Host": config["blog_hostname"]}
551+ url = urlunparse(("http", service_ip, uri, "", "", ""))
552+ if payload:
553+ r = requests.post(url, allow_redirects=False, headers=headers, data=payload, timeout=30)
554+ else:
555+ r = requests.get(url, allow_redirects=False, headers=headers, timeout=30)
556+ if redirects and r.is_redirect:
557+ # Recurse, but strip the scheme and host first, we need to connect over HTTP by bare IP
558+ o = urlparse(r.headers.get("Location"))
559+ return self.call_wordpress(o.path, redirects=redirects, payload=payload, _depth=_depth + 1)
560+ else:
561+ return r
562+ else:
563+ logger.info("Error getting service IP")
564+ return False
565+
566+ def wordpress_configured(self):
567+ """Check whether first install has been completed."""
568+ import requests
569+
570+ # Check whether pod is deployed
571+ if not self.is_pod_up("website"):
572+ return False
573+ # Check if we have WP code deployed at all
574+ if not self.is_vhost_ready():
575+ return False
576+ # We have code on disk, check if configured
577+ try:
578+ r = self.call_wordpress("/", redirects=False)
579+ except requests.exceptions.ConnectionError:
580+ return False
581+ if r.status_code == 302 and re.match("^.*/wp-admin/install.php", r.headers.get("location", "")):
582+ return False
583+ elif r.status_code == 302 and re.match("^.*/wp-admin/setup-config.php", r.headers.get("location", "")):
584+ logger.info("MySQL database setup failed, we likely have no wp-config.php")
585+ self.model.unit.status = BlockedStatus("MySQL database setup failed, we likely have no wp-config.php")
586+ return False
587+ else:
588+ return True
589+
590+ def is_vhost_ready(self):
591+ """Check whether wordpress is available using http."""
592+ import requests
593+
594+ # Check if we have WP code deployed at all
595+ try:
596+ r = self.call_wordpress("/wp-login.php", redirects=False)
597+ except requests.exceptions.ConnectionError:
598+ logger.info("call_wordpress() returned requests.exceptions.ConnectionError")
599+ return False
600+ if r is None:
601+ logger.info("call_wordpress() returned None")
602+ return False
603+ if hasattr(r, "status_code") and r.status_code in (403, 404):
604+ logger.info("call_wordpress() returned status {}".format(r.status_code))
605+ return False
606+ else:
607+ return True
608+
609+ def get_service_ip(self, endpoint):
610+ try:
611+ self.model.get_binding(endpoint).network is not False
612+ except Exception:
613+ logger.info("We don't have networking yet")
614+ return ''
615+ try:
616+ return str(self.model.get_binding(endpoint).network.ingress_addresses[0])
617+ except Exception:
618+ logger.info("We don't have any ingress addresses yet")
619+ return ''
620+
621+ def is_pod_up(self, endpoint):
622+ """Check to see if the pod of a relation is up"""
623+ ingress = self.get_service_ip(endpoint)
624+ if len(ingress) == 0:
625+ return False
626+ return True
627+
628+ def configure_pod(self, event): # NoQA: C901
629+ if not self.state._started:
630+ return
631+ if not self.state._valid:
632+ return
633+ elif self.model.unit.is_leader():
634+ # only the leader can set_spec()
635+ spec = self.make_pod_spec()
636+ if spec is None:
637+ return # Status already set
638+ # TODO: Figure out operator equivalent of reactive.data_changed()
639+ # https://github.com/canonical/operator/issues/189
640+ # if reactive.data_changed("wordpress.spec", spec):
641+ # "if True" works around it, but we'll respwan on every hook run,
642+ # which is decidedly sub-optimal
643+ if True:
644+ self.model.unit.status = MaintenanceStatus("Configuring container")
645+ try:
646+ self.model.pod.set_spec(spec)
647+ except Exception as e:
648+ logger.info("pod_spec_set failed: {}".format(e))
649+ self.model.unit.status = BlockedStatus("pod_spec_set failed! Check logs and k8s dashboard.")
650+ return
651+ else:
652+ logger.info("No changes to pod spec")
653+ if not self.state._configured:
654+ tries = 0
655+ max_tries = 20
656+ while tries < max_tries:
657+ if self.first_install():
658+ self.state._configured = True
659+ self.model.unit.status = ActiveStatus()
660+ else:
661+ tries = tries + 1
662+ logger.info("Sleeping 30s after attempt {}/{}, will try again".format(tries, max_tries))
663+ sleep(30)
664+ logger.error("Giving up on configuration after attempt {}/{}".format(tries, max_tries))
665+ self.model.unit.status = BlockedStatus("first_install failed! Check logs")
666+
667+
668+if __name__ == "__main__":
669+ main(Wordpress)
670diff --git a/tests/unit/test_wordpress.py b/tests/unit/test_wordpress.py
671index 5ade069..e69de29 100644
672--- a/tests/unit/test_wordpress.py
673+++ b/tests/unit/test_wordpress.py
674@@ -1,64 +0,0 @@
675-import os
676-import shutil
677-import sys
678-import tempfile
679-import unittest
680-from unittest import mock
681-
682-# We also need to mock up charms.layer so we can run unit tests without having
683-# to build the charm and pull in layers such as layer-status.
684-sys.modules['charms.layer'] = mock.MagicMock()
685-
686-from charms.layer import status # NOQA: E402
687-
688-# Add path to where our reactive layer lives and import.
689-sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
690-from reactive import wordpress # NOQA: E402
691-
692-
693-class TestCharm(unittest.TestCase):
694- def setUp(self):
695- self.maxDiff = None
696- self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
697- self.addCleanup(shutil.rmtree, self.tmpdir)
698-
699- self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
700-
701- patcher = mock.patch('charmhelpers.core.hookenv.log')
702- self.mock_log = patcher.start()
703- self.addCleanup(patcher.stop)
704- self.mock_log.return_value = ''
705-
706- patcher = mock.patch('charmhelpers.core.hookenv.charm_dir')
707- self.mock_charm_dir = patcher.start()
708- self.addCleanup(patcher.stop)
709- self.mock_charm_dir.return_value = self.charm_dir
710-
711- patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
712- self.mock_local_unit = patcher.start()
713- self.addCleanup(patcher.stop)
714- self.mock_local_unit.return_value = 'mock-wordpress/0'
715-
716- patcher = mock.patch('charmhelpers.core.hookenv.config')
717- self.mock_config = patcher.start()
718- self.addCleanup(patcher.stop)
719- self.mock_config.return_value = {'blog_hostname': 'myblog.example.com'}
720-
721- patcher = mock.patch('charmhelpers.core.host.log')
722- self.mock_log = patcher.start()
723- self.addCleanup(patcher.stop)
724- self.mock_log.return_value = ''
725-
726- status.active.reset_mock()
727- status.blocked.reset_mock()
728- status.maintenance.reset_mock()
729-
730- @mock.patch('charms.reactive.clear_flag')
731- def test_hook_upgrade_charm_flags(self, clear_flag):
732- '''Test correct flags set via upgrade-charm hook'''
733- wordpress.upgrade_charm()
734- self.assertFalse(status.maintenance.assert_called())
735- want = [
736- mock.call('wordpress.configured'),
737- ]
738- self.assertFalse(clear_flag.assert_has_calls(want, any_order=True))
739diff --git a/tox.ini b/tox.ini
740index 7b45934..3f4c39f 100644
741--- a/tox.ini
742+++ b/tox.ini
743@@ -10,7 +10,7 @@ setenv =
744
745 [testenv:unit]
746 commands =
747- pytest --ignore {toxinidir}/tests/functional \
748+ pytest --ignore mod --ignore {toxinidir}/tests/functional \
749 {posargs:-v --cov=reactive --cov-report=term-missing --cov-branch}
750 deps = -r{toxinidir}/tests/unit/requirements.txt
751 -r{toxinidir}/requirements.txt
752@@ -24,16 +24,16 @@ passenv =
753 JUJU_REPOSITORY
754 PATH
755 commands =
756- pytest -v --ignore {toxinidir}/tests/unit {posargs}
757+ pytest -v --ignore mod --ignore {toxinidir}/tests/unit {posargs}
758 deps = -r{toxinidir}/tests/functional/requirements.txt
759 -r{toxinidir}/requirements.txt
760
761 [testenv:black]
762-commands = black --skip-string-normalization --line-length=120 .
763+commands = black --skip-string-normalization --line-length=120 src/ tests/
764 deps = black
765
766 [testenv:lint]
767-commands = flake8
768+commands = flake8 src/ tests/
769 deps = flake8
770
771 [flake8]
772diff --git a/wheelhouse.txt b/wheelhouse.txt
773deleted file mode 100644
774index f229360..0000000
775--- a/wheelhouse.txt
776+++ /dev/null
777@@ -1 +0,0 @@
778-requests

Subscribers

People subscribed via source and target branches