Merge ~pjdc/ubuntu-mirror-charm/+git/ubuntu-mirror-charm:shared-triggers into ubuntu-mirror-charm:master

Proposed by Paul Collins
Status: Merged
Approved by: Paul Collins
Approved revision: 262d992bb2b05049712d32999c02290307657e1c
Merged at revision: 9667b405eac9fc0e223190172d63ea22fc97b1e9
Proposed branch: ~pjdc/ubuntu-mirror-charm/+git/ubuntu-mirror-charm:shared-triggers
Merge into: ubuntu-mirror-charm:master
Prerequisite: ~pjdc/ubuntu-mirror-charm/+git/ubuntu-mirror-charm:remove-python-apt
Diff against target: 300 lines (+205/-17)
8 files modified
.coveragerc (+2/-0)
.gitignore (+2/-0)
hooks/hooks.py (+59/-13)
requirements.txt (+0/-0)
tests/unit/requirements.txt (+7/-0)
tests/unit/test_charm.py (+97/-0)
tests/unit/utils.py (+34/-0)
tox.ini (+4/-4)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Haw Loeung +1 Approve
Review via email: mp+388534@code.launchpad.net

This proposal supersedes a proposal from 2020-07-31.

Commit message

merge triggers where key is the same, which is the case when a unit is both a releases and a cdimage mirror

To post a comment you must log in.
Revision history for this message
Paul Collins (pjdc) wrote :

Note pre-req.

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
Paul Collins (pjdc) wrote :
Revision history for this message
Haw Loeung (hloeung) wrote :

LGTM

review: Approve (+1)
Revision history for this message
Stuart Bishop (stub) wrote :

This seems good. The CallableDict implementation can be cut down significantly, per inline comments.

review: Approve
Revision history for this message
Paul Collins (pjdc) wrote :

Replies for the record.

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

Change successfully merged at revision 9667b405eac9fc0e223190172d63ea22fc97b1e9

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.coveragerc b/.coveragerc
0new file mode 1006440new file mode 100644
index 0000000..f1565c6
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,2 @@
1[run]
2omit = hooks/charmhelpers/*
diff --git a/.gitignore b/.gitignore
index 490cc43..526bc62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
1*~1*~
2*.charm2*.charm
3*.pyc
4*.pyo
3.tox5.tox
4.coverage6.coverage
5__pycache__7__pycache__
diff --git a/hooks/hooks.py b/hooks/hooks.py
index e1f4d53..877fa43 100755
--- a/hooks/hooks.py
+++ b/hooks/hooks.py
@@ -561,17 +561,48 @@ def configure_vsftp(conf, hostname):
561 log("CHARM: Finished configuring vsftp")561 log("CHARM: Finished configuring vsftp")
562562
563563
564#564SSH_TRIGGER_TEMPLATE = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="{command}",{trigger}'
565# Set up any external ssh sync triggers565
566#566
567def configure_triggers(conf, hostname):567# cdimage-builder.internal triggers releases and cdimage using the
568# same key, so when this unit has both roles, sync both of them.
569def merge_shared_key_triggers(conf, triggers):
570 """Merge (in place) triggers that use the same ssh key."""
571
572 cdimage_key = extract_ssh_public_key(triggers.get('cdimage'))
573 releases_key = extract_ssh_public_key(triggers.get('releases'))
574 if cdimage_key and releases_key and cdimage_key == releases_key:
575 releases = conf.mirror_map('releases')
576 cdimage = conf.mirror_map('cdimage')
577 command = '( {releases_command} releases ; {cdimage_command} cdimage ) &'.format(
578 releases_command=os.path.join(conf.script_dir(), releases['command']),
579 cdimage_command=os.path.join(conf.script_dir(), cdimage['command']),
580 )
581 triggers['releases'] = SSH_TRIGGER_TEMPLATE.format(
582 command=command,
583 trigger=releases['trigger'],
584 )
585 del(triggers['cdimage'])
586
587
588def extract_ssh_public_key(key):
589 """Return the field after the field that begins with "ssh-", which
590 should be the key itself. This function can therefore cope with
591 fairly arbitrarily-decorated ssh keys, e.g. those with "from", etc."""
592
593 kelts = key.split(' ')
594 while kelts:
595 # This doesn't work for every type of key, but nobody should be using ECDSA anyway.
596 if kelts.pop(0).startswith('ssh-'):
597 return kelts[0]
598 return None
599
600
601def make_triggers(conf, hostname):
602 """Return this unit's triggers in a dict, keyed by role."""
568 roles = conf.roles()603 roles = conf.roles()
569 if hostname not in roles:
570 return
571604
572 template = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="${command}",${trigger}'605 triggers = {}
573 tmpl_data = {}
574 contents = ""
575606
576 for role in roles[hostname]:607 for role in roles[hostname]:
577 try:608 try:
@@ -583,15 +614,30 @@ def configure_triggers(conf, hostname):
583 continue614 continue
584 else:615 else:
585 if mirror["trigger"]:616 if mirror["trigger"]:
586 tmpl_data["command"] = "%s %s &" % (os.path.join(conf.script_dir(), mirror["command"]), role)617 triggers[role] = SSH_TRIGGER_TEMPLATE.format(
587 tmpl_data["trigger"] = mirror["trigger"]618 command="%s %s &" % (os.path.join(conf.script_dir(), mirror["command"]), role),
588 contents += str(Template(template, tmpl_data)) + "\n"619 trigger=mirror["trigger"],
620 )
621
622 return triggers
623
624
625#
626# Set up any external ssh sync triggers
627#
628def configure_triggers(conf, hostname):
629 roles = conf.roles()
630 if hostname not in roles:
631 return
632
633 triggers = make_triggers(conf, hostname)
634 merge_shared_key_triggers(conf, triggers)
589635
590 keyfile = get_ssh_keyfile(conf.mirror_user())636 keyfile = get_ssh_keyfile(conf.mirror_user())
591 ssh_dir = os.path.dirname(keyfile)637 ssh_dir = os.path.dirname(keyfile)
592 if not os.path.isdir(ssh_dir):638 if not os.path.isdir(ssh_dir):
593 mkdir(ssh_dir)639 mkdir(ssh_dir)
594 write_file(keyfile, contents)640 write_file(keyfile, '\n'.join(triggers.values()) + '\n')
595641
596642
597def configure_nrpe(conf, hostname): # noqa: C901643def configure_nrpe(conf, hostname): # noqa: C901
diff --git a/requirements.txt b/requirements.txt
598new file mode 100644644new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/requirements.txt
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
599new file mode 100644645new file mode 100644
index 0000000..449073d
--- /dev/null
+++ b/tests/unit/requirements.txt
@@ -0,0 +1,7 @@
1# test requirements
2mock
3pytest
4pytest-cov
5# charm requirements
6Cheetah
7pyyaml
diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
0new file mode 1006448new file mode 100644
index 0000000..41508f6
--- /dev/null
+++ b/tests/unit/test_charm.py
@@ -0,0 +1,97 @@
1import json
2import unittest
3
4from base64 import b64encode
5from copy import deepcopy
6from mock import patch
7
8import Config # for mockery
9import hooks # noqa: F401; for mockery
10
11from hooks import (
12 SSH_TRIGGER_TEMPLATE,
13 configure_triggers,
14 extract_ssh_public_key,
15 make_triggers,
16 merge_shared_key_triggers,
17)
18
19from utils import (
20 CallableDict,
21 read_config_yaml,
22)
23
24BARE_RELEASES_SSH_KEY = "AAAAB3NzaC1yc2EAAAABIwAAAQEA7TIXsws/s48o4N1MA+TpaudHS8XVF79JujYQGTumroZZqbsr04lbg1FaJ9HyS9P31e8lP2bmB5rexJaMyfAxDTV+DtrHXmeLi/XcGSOXoliOa/j3eQmdZyYme9z+Tfp8s3S6nxuDP3BRKkOuIzOPgPO5fr8AH7y83v3fyHi+H0sbm6agX2b+xV9oP8+DEgSxPNfZEVTgpdJ7/ULwAl68mWQW0w6VTVx0CMYF+cvcorcZMGmy+THzk8WH2XRalA5HqeiBtS7vJ+yFHJ5WbQOEUeR6gHRcrogR4Yd10eFWUNAFyvt53zF7W91PEgnJWjcCUN/uctybAFSgDK1UB6TPkQ==" # noqa: E501
25RELEASES_SSH_KEY = 'from="91.189.90.151,91.189.89.127,10.22.96.61,10.22.96.161" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA7TIXsws/s48o4N1MA+TpaudHS8XVF79JujYQGTumroZZqbsr04lbg1FaJ9HyS9P31e8lP2bmB5rexJaMyfAxDTV+DtrHXmeLi/XcGSOXoliOa/j3eQmdZyYme9z+Tfp8s3S6nxuDP3BRKkOuIzOPgPO5fr8AH7y83v3fyHi+H0sbm6agX2b+xV9oP8+DEgSxPNfZEVTgpdJ7/ULwAl68mWQW0w6VTVx0CMYF+cvcorcZMGmy+THzk8WH2XRalA5HqeiBtS7vJ+yFHJ5WbQOEUeR6gHRcrogR4Yd10eFWUNAFyvt53zF7W91PEgnJWjcCUN/uctybAFSgDK1UB6TPkQ== cjwatson@little' # noqa: E501
26
27
28MERGEABLE_TRIGGERS_CONFIG = {
29 'mirror_cdimage_trigger': b64encode(RELEASES_SSH_KEY),
30 'mirror_releases_trigger': b64encode(RELEASES_SSH_KEY),
31 'role_map': json.dumps({
32 'localhost': ['cdimage', 'releases'],
33 })
34}
35
36MERGEABLE_TRIGGERS = {
37 'cdimage': SSH_TRIGGER_TEMPLATE.format(
38 command='/srv/ubuntu-mirror/bin/mirror-1stage.sh cdimage &',
39 trigger=RELEASES_SSH_KEY,
40 ),
41 'releases': SSH_TRIGGER_TEMPLATE.format(
42 command='/srv/ubuntu-mirror/bin/mirror-1stage.sh releases &',
43 trigger=RELEASES_SSH_KEY,
44 ),
45}
46
47MERGED_TRIGGERS = {
48 'releases': SSH_TRIGGER_TEMPLATE.format(
49 command='( /srv/ubuntu-mirror/bin/mirror-1stage.sh releases ; /srv/ubuntu-mirror/bin/mirror-1stage.sh cdimage ) &', # noqa: E501
50 trigger=RELEASES_SSH_KEY,
51 ),
52}
53
54
55MERGEABLE_TRIGGERS_SSH_AUTHORIZED_KEYS = 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="( /srv/ubuntu-mirror/bin/mirror-1stage.sh releases ; /srv/ubuntu-mirror/bin/mirror-1stage.sh cdimage ) &",from="91.189.90.151,91.189.89.127,10.22.96.61,10.22.96.161" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA7TIXsws/s48o4N1MA+TpaudHS8XVF79JujYQGTumroZZqbsr04lbg1FaJ9HyS9P31e8lP2bmB5rexJaMyfAxDTV+DtrHXmeLi/XcGSOXoliOa/j3eQmdZyYme9z+Tfp8s3S6nxuDP3BRKkOuIzOPgPO5fr8AH7y83v3fyHi+H0sbm6agX2b+xV9oP8+DEgSxPNfZEVTgpdJ7/ULwAl68mWQW0w6VTVx0CMYF+cvcorcZMGmy+THzk8WH2XRalA5HqeiBtS7vJ+yFHJ5WbQOEUeR6gHRcrogR4Yd10eFWUNAFyvt53zF7W91PEgnJWjcCUN/uctybAFSgDK1UB6TPkQ== cjwatson@little\n' # noqa: E501
56
57
58class TestUbuntuMirrorCharm(unittest.TestCase):
59 def setUp(self):
60 self.default_config = read_config_yaml()
61 self.maxDiff = None
62
63 def test_extract_ssh_public_key(self):
64 self.assertEqual(extract_ssh_public_key(RELEASES_SSH_KEY), BARE_RELEASES_SSH_KEY)
65
66 @patch('Config.config')
67 def test_make_triggers(self, _config):
68 config = CallableDict(self.default_config)
69 config.update(MERGEABLE_TRIGGERS_CONFIG)
70 _config.side_effect = config # i.e. we want our CallableDict to supply the charm config
71 conf = Config.Config()
72 triggers = make_triggers(conf, 'localhost')
73 self.assertEqual(triggers, MERGEABLE_TRIGGERS)
74
75 @patch('Config.config')
76 def test_merge_shared_key_triggers(self, _config):
77 config = CallableDict(self.default_config)
78 config.update(MERGEABLE_TRIGGERS_CONFIG)
79 _config.side_effect = config # i.e. we want our CallableDict to supply the charm config
80 conf = Config.Config()
81 triggers = deepcopy(MERGEABLE_TRIGGERS)
82 merge_shared_key_triggers(conf, triggers)
83 self.assertEqual(triggers, MERGED_TRIGGERS)
84
85 @patch('hooks.os.path.isdir')
86 @patch('hooks.get_ssh_keyfile')
87 @patch('hooks.write_file')
88 @patch('Config.config')
89 def test_configure_triggers(self, _config, _write_file, _get_ssh_keyfile, _isdir):
90 config = CallableDict(self.default_config)
91 config.update(MERGEABLE_TRIGGERS_CONFIG)
92 _config.side_effect = config # i.e. we want our CallableDict to supply the charm config
93 conf = Config.Config()
94 _get_ssh_keyfile.return_value = '/etc/ssh/user-authorized-keys/{}'.format(config('mirror_user'))
95 _isdir.return_value = True
96 configure_triggers(conf, 'localhost')
97 _write_file.assert_called_with(_get_ssh_keyfile.return_value, MERGEABLE_TRIGGERS_SSH_AUTHORIZED_KEYS)
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
0new file mode 10064498new file mode 100644
index 0000000..1e7cffd
--- /dev/null
+++ b/tests/unit/utils.py
@@ -0,0 +1,34 @@
1import yaml
2
3
4# The Config class fails if config items aren't present, so it's
5# simplest to just read config.yaml and mock hookenv.config().
6def read_config_yaml():
7 """Read config.yaml and return a dict keyed by setting, with their
8 defaults as values, stringified a la config-get."""
9
10 with open('config.yaml') as config_yaml:
11 config = yaml.safe_load(config_yaml)
12 return {k: str(v['default']) for k, v in config['options'].items() if 'default' in v}
13
14
15# Used to mock hookenv.config().
16class CallableDict(dict):
17 """Wrapper for dict type: d(x) <==> d[x]. Intended as a drop-in
18 replacement (or mock) for functions such as config() while
19 allowing responses to be easily configured. Example use, which
20 assumes that something() is imported from somelib, and somelib did
21 "from charmhelpers.core.hookenv import config":
22
23 from somelib import something
24
25 class TestSomething(unittest.TestCase):
26 @patch('somelib.config')
27 def test_something(self, _config):
28 _config.side_effect = CallableDict({'key': 'value'})
29 self.assertTrue(something())
30
31 """
32
33 def __call__(self, *args, **kwargs):
34 return self.__getitem__(*args, **kwargs)
diff --git a/tox.ini b/tox.ini
index 7898f03..39a574c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,13 +10,13 @@ setenv =
1010
11[testenv:unit]11[testenv:unit]
12commands =12commands =
13 pytest --ignore mod --ignore {toxinidir}/tests/functional \13 pytest --ignore {toxinidir}/tests/functional --cov-config={toxinidir}/.coveragerc \
14 {posargs:-v --cov=src --cov-report=term-missing --cov-branch}14 {posargs:-v --cov=hooks --cov-report=term-missing --cov-branch}
15deps = -r{toxinidir}/tests/unit/requirements.txt15deps = -r{toxinidir}/tests/unit/requirements.txt
16 -r{toxinidir}/requirements.txt16 -r{toxinidir}/requirements.txt
17setenv =17setenv =
18 PYTHONPATH={toxinidir}/src:{toxinidir}/build/lib:{toxinidir}/build/venv18 PYTHONPATH = {toxinidir}/hooks
19 TZ=UTC19 TZ = UTC
2020
21[testenv:functional]21[testenv:functional]
22passenv =22passenv =

Subscribers

People subscribed via source and target branches