Merge ~peter-sabaini/charm-sudo-pair:feature/add-pd-logging into ~sudo-pair-charmers/charm-sudo-pair:master
- Git
- lp:~peter-sabaini/charm-sudo-pair
- feature/add-pd-logging
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~peter-sabaini/charm-sudo-pair:feature/add-pd-logging |
Merge into: | ~sudo-pair-charmers/charm-sudo-pair:master |
Diff against target: |
1981 lines (+857/-382) 22 files modified
.gitignore (+24/-16) Makefile (+60/-33) dev/null (+0/-185) interfaces/.empty (+0/-0) layers/.empty (+0/-0) src/README.md (+1/-1) src/actions/actions.py (+19/-2) src/actions/remove-sudopair (+1/-0) src/config.yaml (+8/-0) src/copyright (+16/-0) src/files/pagerdutyevent.py (+68/-0) src/lib/libsudopair.py (+237/-0) src/metadata.yaml (+1/-1) src/reactive/sudo_pair.py (+25/-10) src/templates/sudo_approve.tmpl (+5/-2) src/templates/sudo_pair.pagerduty.tmpl (+6/-0) src/tests/functional/conftest.py (+51/-38) src/tests/functional/test_deploy.py (+53/-66) src/tests/unit/conftest.py (+11/-5) src/tests/unit/test_actions.py (+23/-0) src/tests/unit/test_libsudopair.py (+206/-0) src/tox.ini (+42/-23) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Giuseppe Petralia | Pending | ||
Paul Goins | Pending | ||
Review via email: mp+403955@code.launchpad.net |
This proposal supersedes a proposal from 2020-11-23.
This proposal has been superseded by a proposal from 2021-06-09.
Commit message
Pagerduty alerting, action logging
Ability to have pagerduty alerts triggered on auto-approve. Revamp logging, add logging for remove-sudopair action. Use https proxy to contact the PD events endpoint if set in model config.
Description of the change
Giuseppe Petralia (peppepetra) wrote : Posted in a previous version of this proposal | # |
Paul Goins (vultaire) wrote : Posted in a previous version of this proposal | # |
I agree with Giuseppe. I think we should pull the proxy from the env's JUJU_CHARM_
Peter Sabaini (peter-sabaini) wrote : Posted in a previous version of this proposal | # |
Thanks -- proxy config updated as requested. I've resubmitted the branch as there was some code reorg interim.
Celia Wang (ziyiwang) wrote : Posted in a previous version of this proposal | # |
LGTM.
Paul Goins (vultaire) wrote : Posted in a previous version of this proposal | # |
Sorry for the delay here - It looks like this is close to if not identical to https:/
Unmerged commits
- 3681b10... by Peter Sabaini
-
Pagerduty alerting, action logging
Ability to have pagerduty alerts triggered on auto-approve. Revamp
logging, add logging for remove-sudopair action. Use https proxy to
contact the PD events endpoint if set in model config. - 0e31f83... by Alvaro Uria
-
Merge remote-tracking branch 'drew/copyright'
Reviewed-on: https:/
/code.launchpad .net/~afreiberg er/charm- sudo-pair/ +git/charm- sudo-pair/ +merge/ 392422
Reviewed-by: Alvaro Uria <email address hidden> - 7628722... by Drew Freiberger
-
Update copyright to 2018-2020
- 24bbbb2... by Giuseppe Petralia
-
Add copyright Apache-2
- 7e3bae4... by Giuseppe Petralia
-
Update noqa comments to do a single check
- 625fd16... by Giuseppe Petralia
-
Fix flake8/black conflicts.
- 98e3587... by Giuseppe Petralia
-
Extra Linting completed for 20.08 charm release
- 224d950... by Giuseppe Petralia
-
Blackened repository to 88 lines
- 421e685... by Giuseppe Petralia
-
Imported standard Makefile and tox.ini and fixed up tests
- de75f72... by Adam Dyess
-
Resolve issue where workload status isn't updated after remove-sudopair action
Preview Diff
1 | diff --git a/.gitignore b/.gitignore | |||
2 | index eda8bea..6f1f367 100644 | |||
3 | --- a/.gitignore | |||
4 | +++ b/.gitignore | |||
5 | @@ -1,33 +1,41 @@ | |||
6 | 1 | # Juju files | ||
7 | 2 | .unit-state.db | ||
8 | 3 | .go-cookies | ||
9 | 4 | |||
10 | 5 | layers/* | ||
11 | 6 | interfaces/* | ||
12 | 7 | |||
13 | 1 | # Byte-compiled / optimized / DLL files | 8 | # Byte-compiled / optimized / DLL files |
14 | 2 | __pycache__/ | 9 | __pycache__/ |
15 | 3 | *.py[cod] | 10 | *.py[cod] |
16 | 4 | *$py.class | 11 | *$py.class |
17 | 5 | 12 | ||
18 | 13 | # Tests files and dir | ||
19 | 14 | .pytest_cache/ | ||
20 | 15 | .coverage | ||
21 | 16 | .tox | ||
22 | 17 | report/ | ||
23 | 18 | htmlcov/ | ||
24 | 19 | |||
25 | 6 | # Log files | 20 | # Log files |
26 | 7 | *.log | 21 | *.log |
27 | 8 | 22 | ||
31 | 9 | .tox/ | 23 | # pycharm |
32 | 10 | src/.tox/ | 24 | .idea/ |
30 | 11 | .coverage | ||
33 | 12 | 25 | ||
34 | 13 | # vi | 26 | # vi |
35 | 14 | .*.swp | 27 | .*.swp |
36 | 15 | 28 | ||
37 | 16 | # pycharm | ||
38 | 17 | .idea/ | ||
39 | 18 | .unit-state.db | ||
40 | 19 | src/.unit-state.db | ||
41 | 20 | |||
42 | 21 | # version data | 29 | # version data |
43 | 22 | repo-info | 30 | repo-info |
44 | 31 | version | ||
45 | 23 | 32 | ||
46 | 33 | # Python builds | ||
47 | 34 | deb_dist/ | ||
48 | 35 | dist/ | ||
49 | 24 | 36 | ||
56 | 25 | # reports | 37 | # Snaps |
57 | 26 | report/* | 38 | *.snap |
52 | 27 | src/report/* | ||
53 | 28 | |||
54 | 29 | # virtual env | ||
55 | 30 | venv/* | ||
58 | 31 | 39 | ||
59 | 32 | # builds | ||
60 | 33 | builds/* | ||
61 | 34 | \ No newline at end of file | 40 | \ No newline at end of file |
62 | 41 | # Builds | ||
63 | 42 | .build/ | ||
64 | 35 | \ No newline at end of file | 43 | \ No newline at end of file |
65 | diff --git a/Makefile b/Makefile | |||
66 | index c7506b2..0a84b5f 100644 | |||
67 | --- a/Makefile | |||
68 | +++ b/Makefile | |||
69 | @@ -1,53 +1,80 @@ | |||
72 | 1 | PROJECTPATH = $(dir $(realpath $(MAKEFILE_LIST))) | 1 | PYTHON := /usr/bin/python3 |
71 | 2 | DIRNAME = $(notdir $(PROJECTPATH:%/=%)) | ||
73 | 3 | 2 | ||
74 | 3 | PROJECTPATH=$(dir $(realpath $(MAKEFILE_LIST))) | ||
75 | 4 | ifndef CHARM_BUILD_DIR | 4 | ifndef CHARM_BUILD_DIR |
78 | 5 | CHARM_BUILD_DIR := /tmp/$(DIRNAME)-builds | 5 | CHARM_BUILD_DIR=${PROJECTPATH}.build |
77 | 6 | $(warning Warning CHARM_BUILD_DIR was not set, defaulting to $(CHARM_BUILD_DIR)) | ||
79 | 7 | endif | 6 | endif |
80 | 7 | ifndef CHARM_LAYERS_DIR | ||
81 | 8 | CHARM_LAYERS_DIR=${PROJECTPATH}/layers | ||
82 | 9 | endif | ||
83 | 10 | ifndef CHARM_INTERFACES_DIR | ||
84 | 11 | CHARM_INTERFACES_DIR=${PROJECTPATH}/interfaces | ||
85 | 12 | endif | ||
86 | 13 | METADATA_FILE="src/metadata.yaml" | ||
87 | 14 | CHARM_NAME=$(shell cat ${PROJECTPATH}/${METADATA_FILE} | grep -E "^name:" | awk '{print $$2}') | ||
88 | 8 | 15 | ||
89 | 9 | help: | 16 | help: |
90 | 10 | @echo "This project supports the following targets" | 17 | @echo "This project supports the following targets" |
91 | 11 | @echo "" | 18 | @echo "" |
92 | 12 | @echo " make help - show this text" | 19 | @echo " make help - show this text" |
93 | 13 | @echo " make lint - run flake8" | ||
94 | 14 | @echo " make test - run the functional tests, unittests and lint" | ||
95 | 15 | @echo " make unittest - run the tests defined in the unittest subdirectory" | ||
96 | 16 | @echo " make functional - run the tests defined in the functional subdirectory" | ||
97 | 17 | @echo " make release - build the charm" | ||
98 | 18 | @echo " make clean - remove unneeded files" | 20 | @echo " make clean - remove unneeded files" |
99 | 21 | @echo " make submodules - make sure that the submodules are up-to-date" | ||
100 | 22 | @echo " make submodules-update - update submodules to latest changes on remote branch" | ||
101 | 23 | @echo " make build - build the charm" | ||
102 | 24 | @echo " make release - run clean, submodules, and build targets" | ||
103 | 25 | @echo " make lint - run flake8 and black --check" | ||
104 | 26 | @echo " make black - run black and reformat files" | ||
105 | 27 | @echo " make proof - run charm proof" | ||
106 | 28 | @echo " make unittests - run the tests defined in the unittest subdirectory" | ||
107 | 29 | @echo " make functional - run the tests defined in the functional subdirectory" | ||
108 | 30 | @echo " make test - run lint, proof, unittests and functional targets" | ||
109 | 19 | @echo "" | 31 | @echo "" |
110 | 20 | 32 | ||
116 | 21 | lint: | 33 | clean: |
117 | 22 | @echo "Running flake8" | 34 | @echo "Cleaning files" |
118 | 23 | @tox -e lint | 35 | @git clean -ffXd -e '!.idea' |
119 | 24 | 36 | @echo "Cleaning existing build" | |
120 | 25 | test: lint unittest functional | 37 | @rm -rf ${CHARM_BUILD_DIR}/${CHARM_NAME} |
121 | 26 | 38 | ||
127 | 27 | functional: build | 39 | submodules: |
128 | 28 | @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \ | 40 | # @git submodule update --init --recursive |
129 | 29 | PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \ | 41 | @echo "No submodules. Skipping." |
125 | 30 | PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \ | ||
126 | 31 | tox -e functional | ||
130 | 32 | 42 | ||
133 | 33 | unittest: | 43 | submodules-update: |
134 | 34 | @tox -e unit | 44 | # @git submodule update --init --recursive --remote --merge |
135 | 45 | @echo "No submodules. Skipping." | ||
136 | 35 | 46 | ||
137 | 36 | build: | 47 | build: |
142 | 37 | @echo "Building charm to base directory $(CHARM_BUILD_DIR)" | 48 | @echo "Building charm to directory ${CHARM_BUILD_DIR}/${CHARM_NAME}" |
143 | 38 | @-git describe --tags > ./repo-info | 49 | @-git rev-parse --abbrev-ref HEAD > ./src/repo-info |
144 | 39 | @CHARM_LAYERS_DIR=./layers CHARM_INTERFACES_DIR=./interfaces TERM=linux\ | 50 | @CHARM_LAYERS_DIR=${CHARM_LAYERS_DIR} CHARM_INTERFACES_DIR=${CHARM_INTERFACES_DIR} \ |
145 | 40 | charm build --output-dir $(CHARM_BUILD_DIR) $(PROJECTPATH) --force | 51 | TERM=linux CHARM_BUILD_DIR=${CHARM_BUILD_DIR} charm build src/ |
146 | 41 | 52 | ||
147 | 42 | release: clean build | 53 | release: clean build |
149 | 43 | @echo "Charm is built at $(CHARM_BUILD_DIR)/builds" | 54 | @echo "Charm is built at ${CHARM_BUILD_DIR}/${CHARM_NAME}" |
150 | 44 | 55 | ||
157 | 45 | clean: | 56 | lint: |
158 | 46 | @echo "Cleaning files" | 57 | @echo "Running lint checks" |
159 | 47 | @find $(PROJECTPATH) -iname __pycache__ -exec rm -r {} + | 58 | @cd src && tox -e lint |
160 | 48 | @if [ -d $(CHARM_BUILD_DIR)/builds ] ; then rm -r $(CHARM_BUILD_DIR)/builds ; fi | 59 | |
161 | 49 | @if [ -d $(PROJECTPATH)/.tox ] ; then rm -r $(PROJECTPATH)/.tox ; fi | 60 | black: |
162 | 50 | @if [ -d $(PROJECTPATH)/.pytest_cache ] ; then rm -r $(PROJECTPATH)/.pytest_cache ; fi | 61 | @echo "Reformat files with black" |
163 | 62 | @cd src && tox -e black | ||
164 | 63 | |||
165 | 64 | proof: build | ||
166 | 65 | @echo "Running charm proof" | ||
167 | 66 | @charm proof ${CHARM_BUILD_DIR}/${CHARM_NAME} | ||
168 | 67 | |||
169 | 68 | unittests: | ||
170 | 69 | @echo "Running unit tests" | ||
171 | 70 | @cd src && tox -e unit | ||
172 | 71 | |||
173 | 72 | functional: build | ||
174 | 73 | @echo "Executing functional tests in ${CHARM_BUILD_DIR}" | ||
175 | 74 | @cd src && CHARM_BUILD_DIR=${CHARM_BUILD_DIR} tox -e func | ||
176 | 75 | |||
177 | 76 | test: lint proof unittests functional | ||
178 | 77 | @echo "Tests completed for charm ${CHARM_NAME}." | ||
179 | 51 | 78 | ||
180 | 52 | # The targets below don't depend on a file | 79 | # The targets below don't depend on a file |
182 | 53 | .PHONY: lint test unittest functional build release clean help | 80 | .PHONY: help submodules submodules-update clean build release lint black proof unittests functional test |
183 | diff --git a/actions/remove-sudopair b/actions/remove-sudopair | |||
184 | 54 | deleted file mode 120000 | 81 | deleted file mode 120000 |
185 | index ff9536b..0000000 | |||
186 | --- a/actions/remove-sudopair | |||
187 | +++ /dev/null | |||
188 | @@ -1 +0,0 @@ | |||
189 | 1 | ./actions.py | ||
190 | 2 | \ No newline at end of file | 0 | \ No newline at end of file |
191 | diff --git a/interfaces/.empty b/interfaces/.empty | |||
192 | 3 | new file mode 100644 | 1 | new file mode 100644 |
193 | index 0000000..e69de29 | |||
194 | --- /dev/null | |||
195 | +++ b/interfaces/.empty | |||
196 | diff --git a/layers/.empty b/layers/.empty | |||
197 | 4 | new file mode 100644 | 2 | new file mode 100644 |
198 | index 0000000..e69de29 | |||
199 | --- /dev/null | |||
200 | +++ b/layers/.empty | |||
201 | diff --git a/lib/libsudopair.py b/lib/libsudopair.py | |||
202 | 5 | deleted file mode 100644 | 3 | deleted file mode 100644 |
203 | index 05d1f9d..0000000 | |||
204 | --- a/lib/libsudopair.py | |||
205 | +++ /dev/null | |||
206 | @@ -1,151 +0,0 @@ | |||
207 | 1 | import grp | ||
208 | 2 | import os | ||
209 | 3 | |||
210 | 4 | from charmhelpers.core import hookenv, host, templating | ||
211 | 5 | |||
212 | 6 | |||
213 | 7 | def check_valid_group(group_name): | ||
214 | 8 | """Check that a group exists.""" | ||
215 | 9 | try: | ||
216 | 10 | grp.getgrnam(group_name) | ||
217 | 11 | return True | ||
218 | 12 | except KeyError: | ||
219 | 13 | return False | ||
220 | 14 | |||
221 | 15 | |||
222 | 16 | def group_id(group_name): | ||
223 | 17 | """Check that a group exists.""" | ||
224 | 18 | return grp.getgrnam(group_name).gr_gid | ||
225 | 19 | |||
226 | 20 | |||
227 | 21 | def group_names_to_group_ids(group_names): | ||
228 | 22 | """ | ||
229 | 23 | Return comma-separated list of Group Ids. | ||
230 | 24 | |||
231 | 25 | :param group_names: i.e. "root,user1,user2" | ||
232 | 26 | :return gids: i.e. "0,1001,1002" | ||
233 | 27 | """ | ||
234 | 28 | group_names = list(filter(check_valid_group, group_names.split(','))) | ||
235 | 29 | return ','.join(map(str, (map(group_id, group_names)))) | ||
236 | 30 | |||
237 | 31 | |||
238 | 32 | def copy_file(source, destination, owner, group, perms): | ||
239 | 33 | """Copy a file on the unit.""" | ||
240 | 34 | if destination is not None: | ||
241 | 35 | target_dir = os.path.dirname(destination) | ||
242 | 36 | if not os.path.exists(target_dir): | ||
243 | 37 | # This is a terrible default directory permission, as the file | ||
244 | 38 | # or its siblings will often contain secrets. | ||
245 | 39 | host.mkdir(os.path.dirname(destination), owner, group, perms=0o755) | ||
246 | 40 | with open(source, 'rb') as source_f: | ||
247 | 41 | host.write_file(destination, source_f.read(), perms=perms, owner=owner, group=group) | ||
248 | 42 | |||
249 | 43 | |||
250 | 44 | class SudoPairHelper(object): | ||
251 | 45 | """Configure sudo-pair.""" | ||
252 | 46 | |||
253 | 47 | def __init__(self): | ||
254 | 48 | """Retrieve charm config and set defaults.""" | ||
255 | 49 | self.charm_config = hookenv.config() | ||
256 | 50 | self.binary_path = '/usr/bin/sudo_approve' | ||
257 | 51 | self.sudo_conf_path = '/etc/sudo.conf' | ||
258 | 52 | self.sudoers_path = '/etc/sudoers' | ||
259 | 53 | self.sudo_lib_path = '/usr/lib/sudo/sudo_pair.so' | ||
260 | 54 | self.sudoers_bypass_path = "/etc/sudoers.d/91-bypass-sudopair-cmds" | ||
261 | 55 | self.user_prompt_path = '/etc/sudo_pair.prompt.user' | ||
262 | 56 | self.pair_prompt_path = '/etc/sudo_pair.prompt.pair' | ||
263 | 57 | self.socket_dir = '/var/run/sudo_pair' | ||
264 | 58 | self.tmpfiles_conf = '/usr/lib/tmpfiles.d/sudo_pair.conf' | ||
265 | 59 | self.owner = 'root' | ||
266 | 60 | self.group = 'root' | ||
267 | 61 | self.socket_dir_perms = 0o644 | ||
268 | 62 | self.sudo_pair_so_perms = 0o644 | ||
269 | 63 | self.prompt_perms = 0o644 | ||
270 | 64 | self.sudoers_perms = 0o440 | ||
271 | 65 | self.sudo_conf_perms = 0o644 | ||
272 | 66 | self.sudo_approve_perms = 0o755 | ||
273 | 67 | |||
274 | 68 | def get_config(self): | ||
275 | 69 | """Return config as a dict.""" | ||
276 | 70 | config = { | ||
277 | 71 | 'binary_path': self.binary_path, | ||
278 | 72 | 'user_prompt_path': self.user_prompt_path, | ||
279 | 73 | 'pair_prompt_path': self.pair_prompt_path, | ||
280 | 74 | 'socket_dir': self.socket_dir, | ||
281 | 75 | 'gids_enforced': group_names_to_group_ids(self.charm_config['groups_enforced']), | ||
282 | 76 | 'gids_exempted': group_names_to_group_ids(self.charm_config['groups_exempted']), | ||
283 | 77 | } | ||
284 | 78 | |||
285 | 79 | config.update(self.charm_config) | ||
286 | 80 | return config | ||
287 | 81 | |||
288 | 82 | def set_charm_config(self, charm_config): | ||
289 | 83 | """Update configuration.""" | ||
290 | 84 | self.charm_config = charm_config | ||
291 | 85 | |||
292 | 86 | def render_sudo_conf(self): | ||
293 | 87 | """Render sudo.conf file.""" | ||
294 | 88 | return templating.render('sudo.conf.tmpl', self.sudo_conf_path, self.get_config(), | ||
295 | 89 | perms=self.sudo_conf_perms, owner=self.owner, group=self.group) | ||
296 | 90 | |||
297 | 91 | def create_socket_dir(self): | ||
298 | 92 | """Create socket dir.""" | ||
299 | 93 | host.mkdir(self.socket_dir, perms=self.socket_dir_perms, owner=self.owner, group=self.group) | ||
300 | 94 | |||
301 | 95 | def create_tmpfiles_conf(self): | ||
302 | 96 | """Create temporary conf file.""" | ||
303 | 97 | with open(self.tmpfiles_conf, "w") as f: | ||
304 | 98 | f.write("d {} 0755 - - -\n".format(self.socket_dir)) | ||
305 | 99 | |||
306 | 100 | def install_sudo_pair_so(self): | ||
307 | 101 | """Install sudo-pair lib.""" | ||
308 | 102 | sudo_pair_lib = os.path.join(hookenv.charm_dir(), 'files', 'sudo_pair.so') | ||
309 | 103 | copy_file(sudo_pair_lib, self.sudo_lib_path, self.owner, self.group, self.sudo_pair_so_perms) | ||
310 | 104 | |||
311 | 105 | def copy_user_prompt(self): | ||
312 | 106 | """Copy user prompt on the unit.""" | ||
313 | 107 | prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.user') | ||
314 | 108 | copy_file(prompt_file, self.user_prompt_path, self.owner, self.group, self.prompt_perms) | ||
315 | 109 | |||
316 | 110 | def copy_pair_prompt(self): | ||
317 | 111 | """Copy pair prompt on the unit.""" | ||
318 | 112 | prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.pair') | ||
319 | 113 | copy_file(prompt_file, self.pair_prompt_path, self.owner, self.group, self.prompt_perms) | ||
320 | 114 | |||
321 | 115 | def copy_sudoers(self): | ||
322 | 116 | """Copy sudoers file on the unit.""" | ||
323 | 117 | sudoers_file = os.path.join(hookenv.charm_dir(), 'files', 'sudoers') | ||
324 | 118 | copy_file(sudoers_file, self.sudoers_path, self.owner, self.group, self.sudoers_perms) | ||
325 | 119 | |||
326 | 120 | def render_sudo_approve(self): | ||
327 | 121 | """Render sudo-approve file.""" | ||
328 | 122 | hookenv.log("Rendering sudo_approve.tmpl to {}".format(self.binary_path)) | ||
329 | 123 | return templating.render('sudo_approve.tmpl', self.binary_path, self.get_config(), | ||
330 | 124 | perms=self.sudo_approve_perms, owner=self.owner, group=self.group) | ||
331 | 125 | |||
332 | 126 | def render_bypass_cmds(self): | ||
333 | 127 | """Render bypass command file.""" | ||
334 | 128 | if self.get_config()['bypass_cmds'] != "" and self.get_config()['bypass_group'] != "": | ||
335 | 129 | hookenv.log("Render bypass cmds to {}".format(self.sudoers_bypass_path)) | ||
336 | 130 | return templating.render('91-bypass-sudopair-cmds.tmpl', self.sudoers_bypass_path, | ||
337 | 131 | self.get_config(), perms=0o440, owner=self.owner, group=self.group) | ||
338 | 132 | return None | ||
339 | 133 | |||
340 | 134 | def deconfigure(self): | ||
341 | 135 | """Remove sudo-pair configuration.""" | ||
342 | 136 | paths = [ | ||
343 | 137 | self.sudo_conf_path, | ||
344 | 138 | self.sudo_lib_path, | ||
345 | 139 | self.binary_path, | ||
346 | 140 | self.user_prompt_path, | ||
347 | 141 | self.pair_prompt_path, | ||
348 | 142 | self.sudoers_bypass_path, | ||
349 | 143 | self.tmpfiles_conf | ||
350 | 144 | ] | ||
351 | 145 | hookenv.log("Deleting: {}".format(paths)) | ||
352 | 146 | for path in paths: | ||
353 | 147 | try: | ||
354 | 148 | os.unlink(path) | ||
355 | 149 | except Exception as e: | ||
356 | 150 | # We're trying hard to delete all files, even if some might fail | ||
357 | 151 | hookenv.log("Got exception unlinking {}: {}, continuing".format(path, e)) | ||
358 | diff --git a/README.md b/src/README.md | |||
359 | 152 | similarity index 98% | 0 | similarity index 98% |
360 | 153 | rename from README.md | 1 | rename from README.md |
361 | 154 | rename to src/README.md | 2 | rename to src/README.md |
362 | index 0804cca..3c80295 100644 | |||
363 | --- a/README.md | |||
364 | +++ b/src/README.md | |||
365 | @@ -45,7 +45,7 @@ tox -e functional | |||
366 | 45 | 45 | ||
367 | 46 | 46 | ||
368 | 47 | # Contact Information | 47 | # Contact Information |
370 | 48 | Giuseppe Petralia <giuseppe.petralia@canonical.com> | 48 | BootStack Charmers <bootstack-charmers@lists.canonical.com> |
371 | 49 | 49 | ||
372 | 50 | [service]: https://github.com/square/sudo_pair | 50 | [service]: https://github.com/square/sudo_pair |
373 | 51 | [icon guidelines]: https://jujucharms.com/docs/stable/authors-charm-icon | 51 | [icon guidelines]: https://jujucharms.com/docs/stable/authors-charm-icon |
374 | diff --git a/actions.yaml b/src/actions.yaml | |||
375 | 52 | similarity index 100% | 52 | similarity index 100% |
376 | 53 | rename from actions.yaml | 53 | rename from actions.yaml |
377 | 54 | rename to src/actions.yaml | 54 | rename to src/actions.yaml |
378 | diff --git a/actions/actions.py b/src/actions/actions.py | |||
379 | 55 | similarity index 78% | 55 | similarity index 78% |
380 | 56 | rename from actions/actions.py | 56 | rename from actions/actions.py |
381 | 57 | rename to src/actions/actions.py | 57 | rename to src/actions/actions.py |
382 | index ccf3ffc..6ab26a2 100755 | |||
383 | --- a/actions/actions.py | |||
384 | +++ b/src/actions/actions.py | |||
385 | @@ -1,4 +1,5 @@ | |||
386 | 1 | #!/usr/local/sbin/charm-env python3 | 1 | #!/usr/local/sbin/charm-env python3 |
387 | 2 | """Sudo Pair actions.""" | ||
388 | 2 | # | 3 | # |
389 | 3 | # Copyright 2016,2019,2020 Canonical Ltd | 4 | # Copyright 2016,2019,2020 Canonical Ltd |
390 | 4 | # | 5 | # |
391 | @@ -13,10 +14,13 @@ | |||
392 | 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
393 | 14 | # See the License for the specific language governing permissions and | 15 | # See the License for the specific language governing permissions and |
394 | 15 | # limitations under the License. | 16 | # limitations under the License. |
395 | 17 | import logging | ||
396 | 18 | import logging.handlers | ||
397 | 16 | import os | 19 | import os |
398 | 20 | import subprocess | ||
399 | 17 | import sys | 21 | import sys |
400 | 18 | 22 | ||
402 | 19 | from charmhelpers.core.hookenv import action_fail, action_set | 23 | from charmhelpers.core.hookenv import action_fail, action_set, status_set |
403 | 20 | 24 | ||
404 | 21 | 25 | ||
405 | 22 | sys.path.append("lib") | 26 | sys.path.append("lib") |
406 | @@ -24,11 +28,23 @@ sys.path.append("lib") | |||
407 | 24 | import libsudopair # NOQA | 28 | import libsudopair # NOQA |
408 | 25 | 29 | ||
409 | 26 | 30 | ||
410 | 31 | logger = logging.getLogger("sudopair") | ||
411 | 32 | logger.setLevel(logging.INFO) | ||
412 | 33 | handler = logging.handlers.SysLogHandler( | ||
413 | 34 | address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_AUTH | ||
414 | 35 | ) | ||
415 | 36 | logger.addHandler(handler) | ||
416 | 37 | |||
417 | 38 | |||
418 | 27 | def remove(): | 39 | def remove(): |
419 | 28 | """Remove sudo-pair config and binaries.""" | 40 | """Remove sudo-pair config and binaries.""" |
420 | 29 | sph = libsudopair.SudoPairHelper() | 41 | sph = libsudopair.SudoPairHelper() |
421 | 30 | sph.deconfigure() | 42 | sph.deconfigure() |
423 | 31 | action_set({"message": "Successfully removed sudo-pair config and binaries"}) | 43 | msg = "Successfully removed sudo-pair config and binaries" |
424 | 44 | logger.warning(msg) | ||
425 | 45 | subprocess.run(["/usr/bin/pagerdutyevent.py", msg]) | ||
426 | 46 | action_set({"message": msg}) | ||
427 | 47 | status_set("active", msg) | ||
428 | 32 | 48 | ||
429 | 33 | 49 | ||
430 | 34 | # A dictionary of all the defined actions to callables (which take | 50 | # A dictionary of all the defined actions to callables (which take |
431 | @@ -37,6 +53,7 @@ ACTIONS = {"remove-sudopair": remove} | |||
432 | 37 | 53 | ||
433 | 38 | 54 | ||
434 | 39 | def main(args): | 55 | def main(args): |
435 | 56 | """Dispatch actions based on command arguments.""" | ||
436 | 40 | action_name = os.path.basename(args[0]) | 57 | action_name = os.path.basename(args[0]) |
437 | 41 | try: | 58 | try: |
438 | 42 | ACTIONS[action_name]() | 59 | ACTIONS[action_name]() |
439 | diff --git a/src/actions/remove-sudopair b/src/actions/remove-sudopair | |||
440 | 43 | new file mode 120000 | 60 | new file mode 120000 |
441 | index 0000000..ff9536b | |||
442 | --- /dev/null | |||
443 | +++ b/src/actions/remove-sudopair | |||
444 | @@ -0,0 +1 @@ | |||
445 | 1 | ./actions.py | ||
446 | 0 | \ No newline at end of file | 2 | \ No newline at end of file |
447 | diff --git a/config.yaml b/src/config.yaml | |||
448 | 1 | similarity index 86% | 3 | similarity index 86% |
449 | 2 | rename from config.yaml | 4 | rename from config.yaml |
450 | 3 | rename to src/config.yaml | 5 | rename to src/config.yaml |
451 | index 795269b..6089ac9 100644 | |||
452 | --- a/config.yaml | |||
453 | +++ b/src/config.yaml | |||
454 | @@ -19,3 +19,11 @@ options: | |||
455 | 19 | type: boolean | 19 | type: boolean |
456 | 20 | default: true | 20 | default: true |
457 | 21 | description: "If true, auto approval is permitted." | 21 | description: "If true, auto approval is permitted." |
458 | 22 | pagerduty_key: | ||
459 | 23 | type: string | ||
460 | 24 | default: '' | ||
461 | 25 | description: "If set, a pagerduty event will be triggered upon auto-approving" | ||
462 | 26 | pagerduty_context: | ||
463 | 27 | type: string | ||
464 | 28 | default: '' | ||
465 | 29 | description: "Prefix to add to pagerduty events" | ||
466 | diff --git a/src/copyright b/src/copyright | |||
467 | 22 | new file mode 100644 | 30 | new file mode 100644 |
468 | index 0000000..8fc50d9 | |||
469 | --- /dev/null | |||
470 | +++ b/src/copyright | |||
471 | @@ -0,0 +1,16 @@ | |||
472 | 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 | ||
473 | 2 | |||
474 | 3 | Files: * | ||
475 | 4 | Copyright: 2018-2020, Canonical Ltd. | ||
476 | 5 | License: Apache-2.0 | ||
477 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
478 | 7 | not use this file except in compliance with the License. You may obtain | ||
479 | 8 | a copy of the License at | ||
480 | 9 | |||
481 | 10 | http://www.apache.org/licenses/LICENSE-2.0 | ||
482 | 11 | |||
483 | 12 | Unless required by applicable law or agreed to in writing, software | ||
484 | 13 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
485 | 14 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
486 | 15 | License for the specific language governing permissions and limitations | ||
487 | 16 | under the License. | ||
488 | diff --git a/src/files/pagerdutyevent.py b/src/files/pagerdutyevent.py | |||
489 | 0 | new file mode 100644 | 17 | new file mode 100644 |
490 | index 0000000..f33be42 | |||
491 | --- /dev/null | |||
492 | +++ b/src/files/pagerdutyevent.py | |||
493 | @@ -0,0 +1,68 @@ | |||
494 | 1 | #!/usr/bin/env python3 | ||
495 | 2 | """Send events to pagerduty.""" | ||
496 | 3 | |||
497 | 4 | import argparse | ||
498 | 5 | import configparser | ||
499 | 6 | import json | ||
500 | 7 | import logging | ||
501 | 8 | import logging.handlers | ||
502 | 9 | |||
503 | 10 | import requests | ||
504 | 11 | |||
505 | 12 | logger = logging.getLogger("sudopair") | ||
506 | 13 | logger.setLevel(logging.INFO) | ||
507 | 14 | handler = logging.handlers.SysLogHandler( | ||
508 | 15 | address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_AUTH | ||
509 | 16 | ) | ||
510 | 17 | logger.addHandler(handler) | ||
511 | 18 | |||
512 | 19 | |||
513 | 20 | def args(): | ||
514 | 21 | """Get cli args.""" | ||
515 | 22 | parser = argparse.ArgumentParser() | ||
516 | 23 | parser.add_argument("summary") | ||
517 | 24 | return parser.parse_args() | ||
518 | 25 | |||
519 | 26 | |||
520 | 27 | def trigger_incident(pd_key, summary, source, https_proxy=None): | ||
521 | 28 | """Send an event to the PD events endpoint.""" | ||
522 | 29 | header = {"Content-Type": "application/json"} | ||
523 | 30 | |||
524 | 31 | payload = { | ||
525 | 32 | "routing_key": pd_key, | ||
526 | 33 | "event_action": "trigger", | ||
527 | 34 | "payload": {"summary": summary, "source": source, "severity": "info"}, | ||
528 | 35 | } | ||
529 | 36 | if https_proxy: | ||
530 | 37 | proxies = {"https": https_proxy} | ||
531 | 38 | else: | ||
532 | 39 | proxies = None | ||
533 | 40 | response = requests.post( | ||
534 | 41 | "https://events.pagerduty.com/v2/enqueue", | ||
535 | 42 | data=json.dumps(payload), | ||
536 | 43 | proxies=proxies, | ||
537 | 44 | headers=header, | ||
538 | 45 | ) | ||
539 | 46 | |||
540 | 47 | if response.json()["status"] == "success": | ||
541 | 48 | logger.info("Triggered alert with key {}".format(response.json()["dedup_key"])) | ||
542 | 49 | else: | ||
543 | 50 | logger.warning("Failed to send pagerduty alert: {}".format(response.text)) | ||
544 | 51 | |||
545 | 52 | |||
546 | 53 | def get_conf(): | ||
547 | 54 | """Return config options from file.""" | ||
548 | 55 | config = configparser.ConfigParser() | ||
549 | 56 | config.read("/etc/sudo_pair.pagerduty.ini") | ||
550 | 57 | sec = config["DEFAULT"] | ||
551 | 58 | return sec["pagerduty_key"], sec["pagerduty_source"], sec.get("https_proxy") | ||
552 | 59 | |||
553 | 60 | |||
554 | 61 | def main(): | ||
555 | 62 | """Run the script.""" | ||
556 | 63 | pd_key, source, https_proxy = get_conf() | ||
557 | 64 | trigger_incident(pd_key, args().summary, source, https_proxy) | ||
558 | 65 | |||
559 | 66 | |||
560 | 67 | if __name__ == "__main__": | ||
561 | 68 | main() | ||
562 | diff --git a/files/sudo.prompt.pair b/src/files/sudo.prompt.pair | |||
563 | 0 | similarity index 100% | 69 | similarity index 100% |
564 | 1 | rename from files/sudo.prompt.pair | 70 | rename from files/sudo.prompt.pair |
565 | 2 | rename to src/files/sudo.prompt.pair | 71 | rename to src/files/sudo.prompt.pair |
566 | diff --git a/files/sudo.prompt.user b/src/files/sudo.prompt.user | |||
567 | 3 | similarity index 100% | 72 | similarity index 100% |
568 | 4 | rename from files/sudo.prompt.user | 73 | rename from files/sudo.prompt.user |
569 | 5 | rename to src/files/sudo.prompt.user | 74 | rename to src/files/sudo.prompt.user |
570 | diff --git a/files/sudo_pair.so b/src/files/sudo_pair.so | |||
571 | 6 | similarity index 100% | 75 | similarity index 100% |
572 | 7 | rename from files/sudo_pair.so | 76 | rename from files/sudo_pair.so |
573 | 8 | rename to src/files/sudo_pair.so | 77 | rename to src/files/sudo_pair.so |
574 | 9 | Binary files a/files/sudo_pair.so and b/src/files/sudo_pair.so differ | 78 | Binary files a/files/sudo_pair.so and b/src/files/sudo_pair.so differ |
575 | diff --git a/files/sudoers b/src/files/sudoers | |||
576 | 10 | similarity index 100% | 79 | similarity index 100% |
577 | 11 | rename from files/sudoers | 80 | rename from files/sudoers |
578 | 12 | rename to src/files/sudoers | 81 | rename to src/files/sudoers |
579 | diff --git a/layer.yaml b/src/layer.yaml | |||
580 | 13 | similarity index 100% | 82 | similarity index 100% |
581 | 14 | rename from layer.yaml | 83 | rename from layer.yaml |
582 | 15 | rename to src/layer.yaml | 84 | rename to src/layer.yaml |
583 | diff --git a/src/lib/libsudopair.py b/src/lib/libsudopair.py | |||
584 | 16 | new file mode 100644 | 85 | new file mode 100644 |
585 | index 0000000..c66873a | |||
586 | --- /dev/null | |||
587 | +++ b/src/lib/libsudopair.py | |||
588 | @@ -0,0 +1,237 @@ | |||
589 | 1 | """Sudo pair utilities.""" | ||
590 | 2 | import grp | ||
591 | 3 | import os | ||
592 | 4 | |||
593 | 5 | from charmhelpers.core import hookenv, host, templating | ||
594 | 6 | |||
595 | 7 | |||
596 | 8 | def check_valid_group(group_name): | ||
597 | 9 | """Check that a group exists.""" | ||
598 | 10 | try: | ||
599 | 11 | grp.getgrnam(group_name) | ||
600 | 12 | return True | ||
601 | 13 | except KeyError: | ||
602 | 14 | return False | ||
603 | 15 | |||
604 | 16 | |||
605 | 17 | def group_id(group_name): | ||
606 | 18 | """Check that a group exists.""" | ||
607 | 19 | return grp.getgrnam(group_name).gr_gid | ||
608 | 20 | |||
609 | 21 | |||
610 | 22 | def group_names_to_group_ids(group_names): | ||
611 | 23 | """ | ||
612 | 24 | Return comma-separated list of Group Ids. | ||
613 | 25 | |||
614 | 26 | :param group_names: i.e. "root,user1,user2" | ||
615 | 27 | :return gids: i.e. "0,1001,1002" | ||
616 | 28 | """ | ||
617 | 29 | group_names = list(filter(check_valid_group, group_names.split(","))) | ||
618 | 30 | return ",".join(map(str, (map(group_id, group_names)))) | ||
619 | 31 | |||
620 | 32 | |||
621 | 33 | def copy_file(source, destination, owner, group, perms): | ||
622 | 34 | """Copy a file on the unit.""" | ||
623 | 35 | if destination is not None: | ||
624 | 36 | target_dir = os.path.dirname(destination) | ||
625 | 37 | if not os.path.exists(target_dir): | ||
626 | 38 | # This is a terrible default directory permission, as the file | ||
627 | 39 | # or its siblings will often contain secrets. | ||
628 | 40 | host.mkdir(os.path.dirname(destination), owner, group, perms=0o755) | ||
629 | 41 | with open(source, "rb") as source_f: | ||
630 | 42 | host.write_file( | ||
631 | 43 | destination, source_f.read(), perms=perms, owner=owner, group=group | ||
632 | 44 | ) | ||
633 | 45 | |||
634 | 46 | |||
635 | 47 | class SudoPairHelper(object): | ||
636 | 48 | """Configure sudo-pair.""" | ||
637 | 49 | |||
638 | 50 | def __init__(self): | ||
639 | 51 | """Retrieve charm config and set defaults.""" | ||
640 | 52 | self.charm_config = hookenv.config() | ||
641 | 53 | self.binary_path = "/usr/bin/sudo_approve" | ||
642 | 54 | self.sudo_conf_path = "/etc/sudo.conf" | ||
643 | 55 | self.sudoers_path = "/etc/sudoers" | ||
644 | 56 | self.sudo_lib_path = "/usr/lib/sudo/sudo_pair.so" | ||
645 | 57 | self.sudoers_bypass_path = "/etc/sudoers.d/91-bypass-sudopair-cmds" | ||
646 | 58 | self.user_prompt_path = "/etc/sudo_pair.prompt.user" | ||
647 | 59 | self.pair_prompt_path = "/etc/sudo_pair.prompt.pair" | ||
648 | 60 | self.socket_dir = "/var/run/sudo_pair" | ||
649 | 61 | self.tmpfiles_conf = "/usr/lib/tmpfiles.d/sudo_pair.conf" | ||
650 | 62 | self.owner = "root" | ||
651 | 63 | self.group = "root" | ||
652 | 64 | self.socket_dir_perms = 0o644 | ||
653 | 65 | self.sudo_pair_so_perms = 0o644 | ||
654 | 66 | self.prompt_perms = 0o644 | ||
655 | 67 | self.sudoers_perms = 0o440 | ||
656 | 68 | self.sudo_conf_perms = 0o644 | ||
657 | 69 | self.sudo_approve_perms = 0o755 | ||
658 | 70 | self.pagerduty_conf_path = "/etc/sudo_pair.pagerduty.ini" | ||
659 | 71 | self.pagerduty_path = "/usr/bin/pagerdutyevent.py" | ||
660 | 72 | self.pagerduty_perms = 0o755 | ||
661 | 73 | |||
662 | 74 | def get_config(self): | ||
663 | 75 | """Return config as a dict.""" | ||
664 | 76 | config = { | ||
665 | 77 | "binary_path": self.binary_path, | ||
666 | 78 | "user_prompt_path": self.user_prompt_path, | ||
667 | 79 | "pair_prompt_path": self.pair_prompt_path, | ||
668 | 80 | "socket_dir": self.socket_dir, | ||
669 | 81 | "gids_enforced": group_names_to_group_ids( | ||
670 | 82 | self.charm_config["groups_enforced"] | ||
671 | 83 | ), | ||
672 | 84 | "gids_exempted": group_names_to_group_ids( | ||
673 | 85 | self.charm_config["groups_exempted"] | ||
674 | 86 | ), | ||
675 | 87 | "pagerduty_key": self.charm_config.get("pagerduty_key"), | ||
676 | 88 | } | ||
677 | 89 | proxy_settings = hookenv.env_proxy_settings() | ||
678 | 90 | if proxy_settings: | ||
679 | 91 | config["https_proxy"] = proxy_settings.get("https_proxy") | ||
680 | 92 | config.update(self.charm_config) | ||
681 | 93 | return config | ||
682 | 94 | |||
683 | 95 | def set_charm_config(self, charm_config): | ||
684 | 96 | """Update configuration.""" | ||
685 | 97 | self.charm_config = charm_config | ||
686 | 98 | |||
687 | 99 | def copy_pagerduty(self): | ||
688 | 100 | """Copy PD script in place.""" | ||
689 | 101 | pd_file = os.path.join(hookenv.charm_dir(), "files", "pagerdutyevent.py") | ||
690 | 102 | copy_file( | ||
691 | 103 | pd_file, self.pagerduty_path, self.owner, self.group, self.pagerduty_perms | ||
692 | 104 | ) | ||
693 | 105 | |||
694 | 106 | def render_sudo_conf(self): | ||
695 | 107 | """Render sudo.conf file.""" | ||
696 | 108 | return templating.render( | ||
697 | 109 | "sudo.conf.tmpl", | ||
698 | 110 | self.sudo_conf_path, | ||
699 | 111 | self.get_config(), | ||
700 | 112 | perms=self.sudo_conf_perms, | ||
701 | 113 | owner=self.owner, | ||
702 | 114 | group=self.group, | ||
703 | 115 | ) | ||
704 | 116 | |||
705 | 117 | def create_socket_dir(self): | ||
706 | 118 | """Create socket dir.""" | ||
707 | 119 | host.mkdir( | ||
708 | 120 | self.socket_dir, | ||
709 | 121 | perms=self.socket_dir_perms, | ||
710 | 122 | owner=self.owner, | ||
711 | 123 | group=self.group, | ||
712 | 124 | ) | ||
713 | 125 | |||
714 | 126 | def create_tmpfiles_conf(self): | ||
715 | 127 | """Create temporary conf file.""" | ||
716 | 128 | with open(self.tmpfiles_conf, "w") as f: | ||
717 | 129 | f.write("d {} 0755 - - -\n".format(self.socket_dir)) | ||
718 | 130 | |||
719 | 131 | def install_sudo_pair_so(self): | ||
720 | 132 | """Install sudo-pair lib.""" | ||
721 | 133 | sudo_pair_lib = os.path.join(hookenv.charm_dir(), "files", "sudo_pair.so") | ||
722 | 134 | copy_file( | ||
723 | 135 | sudo_pair_lib, | ||
724 | 136 | self.sudo_lib_path, | ||
725 | 137 | self.owner, | ||
726 | 138 | self.group, | ||
727 | 139 | self.sudo_pair_so_perms, | ||
728 | 140 | ) | ||
729 | 141 | |||
730 | 142 | def copy_user_prompt(self): | ||
731 | 143 | """Copy user prompt on the unit.""" | ||
732 | 144 | prompt_file = os.path.join(hookenv.charm_dir(), "files", "sudo.prompt.user") | ||
733 | 145 | copy_file( | ||
734 | 146 | prompt_file, | ||
735 | 147 | self.user_prompt_path, | ||
736 | 148 | self.owner, | ||
737 | 149 | self.group, | ||
738 | 150 | self.prompt_perms, | ||
739 | 151 | ) | ||
740 | 152 | |||
741 | 153 | def copy_pair_prompt(self): | ||
742 | 154 | """Copy pair prompt on the unit.""" | ||
743 | 155 | prompt_file = os.path.join(hookenv.charm_dir(), "files", "sudo.prompt.pair") | ||
744 | 156 | copy_file( | ||
745 | 157 | prompt_file, | ||
746 | 158 | self.pair_prompt_path, | ||
747 | 159 | self.owner, | ||
748 | 160 | self.group, | ||
749 | 161 | self.prompt_perms, | ||
750 | 162 | ) | ||
751 | 163 | |||
752 | 164 | def copy_sudoers(self): | ||
753 | 165 | """Copy sudoers file on the unit.""" | ||
754 | 166 | sudoers_file = os.path.join(hookenv.charm_dir(), "files", "sudoers") | ||
755 | 167 | copy_file( | ||
756 | 168 | sudoers_file, self.sudoers_path, self.owner, self.group, self.sudoers_perms | ||
757 | 169 | ) | ||
758 | 170 | |||
759 | 171 | def render_sudo_approve(self): | ||
760 | 172 | """Render sudo-approve file.""" | ||
761 | 173 | hookenv.log("Rendering sudo_approve.tmpl to {}".format(self.binary_path)) | ||
762 | 174 | return templating.render( | ||
763 | 175 | "sudo_approve.tmpl", | ||
764 | 176 | self.binary_path, | ||
765 | 177 | self.get_config(), | ||
766 | 178 | perms=self.sudo_approve_perms, | ||
767 | 179 | owner=self.owner, | ||
768 | 180 | group=self.group, | ||
769 | 181 | ) | ||
770 | 182 | |||
771 | 183 | def render_bypass_cmds(self): | ||
772 | 184 | """Render bypass command file.""" | ||
773 | 185 | if ( | ||
774 | 186 | self.get_config()["bypass_cmds"] != "" | ||
775 | 187 | and self.get_config()["bypass_group"] != "" | ||
776 | 188 | ): | ||
777 | 189 | hookenv.log("Render bypass cmds to {}".format(self.sudoers_bypass_path)) | ||
778 | 190 | return templating.render( | ||
779 | 191 | "91-bypass-sudopair-cmds.tmpl", | ||
780 | 192 | self.sudoers_bypass_path, | ||
781 | 193 | self.get_config(), | ||
782 | 194 | perms=0o440, | ||
783 | 195 | owner=self.owner, | ||
784 | 196 | group=self.group, | ||
785 | 197 | ) | ||
786 | 198 | return None | ||
787 | 199 | |||
788 | 200 | def render_pagerduty_conf(self): | ||
789 | 201 | """Create the PD script configuration.""" | ||
790 | 202 | pd_source = "{}-{}".format( | ||
791 | 203 | self.charm_config.get("pagerduty_context", "juju"), hookenv.principal_unit() | ||
792 | 204 | ) | ||
793 | 205 | cfg = self.get_config() | ||
794 | 206 | cfg["pagerduty_source"] = pd_source | ||
795 | 207 | hookenv.log( | ||
796 | 208 | "Rendering pagerduty configuration to {}".format(self.pagerduty_conf_path) | ||
797 | 209 | ) | ||
798 | 210 | return templating.render( | ||
799 | 211 | "sudo_pair.pagerduty.tmpl", | ||
800 | 212 | self.pagerduty_conf_path, | ||
801 | 213 | cfg, | ||
802 | 214 | owner=self.owner, | ||
803 | 215 | group=self.group, | ||
804 | 216 | ) | ||
805 | 217 | |||
806 | 218 | def deconfigure(self): | ||
807 | 219 | """Remove sudo-pair configuration.""" | ||
808 | 220 | paths = [ | ||
809 | 221 | self.sudo_conf_path, | ||
810 | 222 | self.sudo_lib_path, | ||
811 | 223 | self.binary_path, | ||
812 | 224 | self.user_prompt_path, | ||
813 | 225 | self.pair_prompt_path, | ||
814 | 226 | self.sudoers_bypass_path, | ||
815 | 227 | self.tmpfiles_conf, | ||
816 | 228 | ] | ||
817 | 229 | hookenv.log("Deleting: {}".format(paths)) | ||
818 | 230 | for path in paths: | ||
819 | 231 | try: | ||
820 | 232 | os.unlink(path) | ||
821 | 233 | except Exception as e: | ||
822 | 234 | # We're trying hard to delete all files, even if some might fail | ||
823 | 235 | hookenv.log( | ||
824 | 236 | "Got exception unlinking {}: {}, continuing".format(path, e) | ||
825 | 237 | ) | ||
826 | diff --git a/metadata.yaml b/src/metadata.yaml | |||
827 | 0 | similarity index 94% | 238 | similarity index 94% |
828 | 1 | rename from metadata.yaml | 239 | rename from metadata.yaml |
829 | 2 | rename to src/metadata.yaml | 240 | rename to src/metadata.yaml |
830 | index fe2a13c..8b6a9d4 100644 | |||
831 | --- a/metadata.yaml | |||
832 | +++ b/src/metadata.yaml | |||
833 | @@ -1,7 +1,7 @@ | |||
834 | 1 | name: sudo-pair | 1 | name: sudo-pair |
835 | 2 | display-name: sudo-pair | 2 | display-name: sudo-pair |
836 | 3 | summary: sudo_pair is a sudo plugin to manage root privileges | 3 | summary: sudo_pair is a sudo plugin to manage root privileges |
838 | 4 | maintainer: LMA Charmers <llama-charmers@lists.ubuntu.com> | 4 | maintainer: BootStack Charmers <bootstack-charmers@lists.canonical.com> |
839 | 5 | description: | | 5 | description: | |
840 | 6 | sudo_pair is a sudo plugin that ensure that if a user tries to get root privileges, | 6 | sudo_pair is a sudo plugin that ensure that if a user tries to get root privileges, |
841 | 7 | he will need an authorization from a pair | 7 | he will need an authorization from a pair |
842 | diff --git a/reactive/sudo_pair.py b/src/reactive/sudo_pair.py | |||
843 | 8 | similarity index 55% | 8 | similarity index 55% |
844 | 9 | rename from reactive/sudo_pair.py | 9 | rename from reactive/sudo_pair.py |
845 | 10 | rename to src/reactive/sudo_pair.py | 10 | rename to src/reactive/sudo_pair.py |
846 | index 974dd66..392383b 100644 | |||
847 | --- a/reactive/sudo_pair.py | |||
848 | +++ b/src/reactive/sudo_pair.py | |||
849 | @@ -1,3 +1,5 @@ | |||
850 | 1 | """Charm reactive hooks.""" | ||
851 | 2 | |||
852 | 1 | from charmhelpers.core import hookenv | 3 | from charmhelpers.core import hookenv |
853 | 2 | 4 | ||
854 | 3 | from charms.reactive import hook, remove_state, set_state, when, when_not | 5 | from charms.reactive import hook, remove_state, set_state, when, when_not |
855 | @@ -7,10 +9,14 @@ from libsudopair import SudoPairHelper | |||
856 | 7 | sph = SudoPairHelper() | 9 | sph = SudoPairHelper() |
857 | 8 | 10 | ||
858 | 9 | 11 | ||
861 | 10 | @when('apt.installed.socat') | 12 | @when("apt.installed.socat") |
862 | 11 | @when_not('sudo-pair.configured') | 13 | @when_not("sudo-pair.configured") |
863 | 12 | def install_sudo_pair(): | 14 | def install_sudo_pair(): |
865 | 13 | # Install sudo_pair.so, create socket dir, copy sudo_approve to /usr/bin, copy prompts to /etc | 15 | """Install sudo pair. |
866 | 16 | |||
867 | 17 | Install sudo_pair.so, create socket dir, copy sudo_approve to /usr/bin | ||
868 | 18 | and copy prompts to /etc. | ||
869 | 19 | """ | ||
870 | 14 | sph.install_sudo_pair_so() | 20 | sph.install_sudo_pair_so() |
871 | 15 | 21 | ||
872 | 16 | sph.create_socket_dir() | 22 | sph.create_socket_dir() |
873 | @@ -26,24 +32,33 @@ def install_sudo_pair(): | |||
874 | 26 | # Add "Defaults log_output to /etc/sudoers | 32 | # Add "Defaults log_output to /etc/sudoers |
875 | 27 | sph.copy_sudoers() | 33 | sph.copy_sudoers() |
876 | 28 | 34 | ||
877 | 35 | # Add pagerduty script and config | ||
878 | 36 | sph.copy_pagerduty() | ||
879 | 37 | sph.render_pagerduty_conf() | ||
880 | 38 | |||
881 | 29 | # If there are cmds to bypass sudo pairing create file unders sudoers.d | 39 | # If there are cmds to bypass sudo pairing create file unders sudoers.d |
882 | 30 | sph.render_bypass_cmds() | 40 | sph.render_bypass_cmds() |
883 | 31 | 41 | ||
884 | 32 | # Add Plugin sudo_pair sudo_pair.so to sudo.conf | 42 | # Add Plugin sudo_pair sudo_pair.so to sudo.conf |
885 | 33 | sph.render_sudo_conf() | 43 | sph.render_sudo_conf() |
886 | 34 | 44 | ||
890 | 35 | set_state('sudo-pair.installed') | 45 | set_state("sudo-pair.installed") |
891 | 36 | set_state('sudo-pair.configured') | 46 | set_state("sudo-pair.configured") |
892 | 37 | hookenv.status_set('active', 'sudo pairing for users groups: [{}]'.format(sph.get_config()['gids_enforced'])) | 47 | hookenv.status_set( |
893 | 48 | "active", | ||
894 | 49 | "sudo pairing for users groups: [{}]".format(sph.get_config()["gids_enforced"]), | ||
895 | 50 | ) | ||
896 | 38 | 51 | ||
897 | 39 | 52 | ||
899 | 40 | @hook('config-changed') | 53 | @hook("config-changed") |
900 | 41 | def reconfigure_sudo_pair_charm(): | 54 | def reconfigure_sudo_pair_charm(): |
901 | 55 | """Run config-changed hook.""" | ||
902 | 42 | sph.set_charm_config(hookenv.config()) | 56 | sph.set_charm_config(hookenv.config()) |
904 | 43 | remove_state('sudo-pair.configured') | 57 | remove_state("sudo-pair.configured") |
905 | 44 | 58 | ||
906 | 45 | 59 | ||
908 | 46 | @hook('stop') | 60 | @hook("stop") |
909 | 47 | def stop(): | 61 | def stop(): |
910 | 62 | """Run stop hook and remove charm configuration.""" | ||
911 | 48 | sph.deconfigure() | 63 | sph.deconfigure() |
913 | 49 | remove_state('sudo-pair.installed') | 64 | remove_state("sudo-pair.installed") |
914 | diff --git a/templates/91-bypass-sudopair-cmds.tmpl b/src/templates/91-bypass-sudopair-cmds.tmpl | |||
915 | 50 | similarity index 100% | 65 | similarity index 100% |
916 | 51 | rename from templates/91-bypass-sudopair-cmds.tmpl | 66 | rename from templates/91-bypass-sudopair-cmds.tmpl |
917 | 52 | rename to src/templates/91-bypass-sudopair-cmds.tmpl | 67 | rename to src/templates/91-bypass-sudopair-cmds.tmpl |
918 | diff --git a/templates/sudo.conf.tmpl b/src/templates/sudo.conf.tmpl | |||
919 | 53 | similarity index 100% | 68 | similarity index 100% |
920 | 54 | rename from templates/sudo.conf.tmpl | 69 | rename from templates/sudo.conf.tmpl |
921 | 55 | rename to src/templates/sudo.conf.tmpl | 70 | rename to src/templates/sudo.conf.tmpl |
922 | diff --git a/templates/sudo_approve.tmpl b/src/templates/sudo_approve.tmpl | |||
923 | 56 | similarity index 96% | 71 | similarity index 96% |
924 | 57 | rename from templates/sudo_approve.tmpl | 72 | rename from templates/sudo_approve.tmpl |
925 | 58 | rename to src/templates/sudo_approve.tmpl | 73 | rename to src/templates/sudo_approve.tmpl |
926 | index 7164b5a..49665d3 100755 | |||
927 | --- a/templates/sudo_approve.tmpl | |||
928 | +++ b/src/templates/sudo_approve.tmpl | |||
929 | @@ -88,7 +88,7 @@ main() { | |||
930 | 88 | declare -r username | 88 | declare -r username |
931 | 89 | 89 | ||
932 | 90 | declare log_line | 90 | declare log_line |
934 | 91 | log_line="$(date "+[%b %d %H:%M:%S] WARNING: ${username} approved is own sudo session.")" | 91 | log_line="WARNING: ${username} approved own sudo session." |
935 | 92 | declare -r log_line | 92 | declare -r log_line |
936 | 93 | 93 | ||
937 | 94 | if [[ "${uid}" -eq "${ruid}" ]]; then | 94 | if [[ "${uid}" -eq "${ruid}" ]]; then |
938 | @@ -97,7 +97,10 @@ main() { | |||
939 | 97 | exit 1 | 97 | exit 1 |
940 | 98 | {% else %} | 98 | {% else %} |
941 | 99 | echo "You are approving your own session. The incident will be logged." | 99 | echo "You are approving your own session. The incident will be logged." |
943 | 100 | echo ${log_line} >> /var/log/sudo_pair.log | 100 | logger -p auth.warn $log_line |
944 | 101 | {% if pagerduty_key %} | ||
945 | 102 | /usr/bin/pagerdutyevent.py "$log_line" | ||
946 | 103 | {% endif %} | ||
947 | 101 | {% endif %} | 104 | {% endif %} |
948 | 102 | fi | 105 | fi |
949 | 103 | 106 | ||
950 | diff --git a/src/templates/sudo_pair.pagerduty.tmpl b/src/templates/sudo_pair.pagerduty.tmpl | |||
951 | 104 | new file mode 100644 | 107 | new file mode 100644 |
952 | index 0000000..2185cf9 | |||
953 | --- /dev/null | |||
954 | +++ b/src/templates/sudo_pair.pagerduty.tmpl | |||
955 | @@ -0,0 +1,6 @@ | |||
956 | 1 | [DEFAULT] | ||
957 | 2 | pagerduty_key: {{ pagerduty_key }} | ||
958 | 3 | pagerduty_source: {{ pagerduty_source }} | ||
959 | 4 | {% if https_proxy -%} | ||
960 | 5 | https_proxy: {{ https_proxy }} | ||
961 | 6 | {% endif %} | ||
962 | 0 | \ No newline at end of file | 7 | \ No newline at end of file |
963 | diff --git a/tests/00-unit b/src/tests/00-unit | |||
964 | 1 | similarity index 100% | 8 | similarity index 100% |
965 | 2 | rename from tests/00-unit | 9 | rename from tests/00-unit |
966 | 3 | rename to src/tests/00-unit | 10 | rename to src/tests/00-unit |
967 | diff --git a/tests/01-functional b/src/tests/01-functional | |||
968 | 4 | similarity index 100% | 11 | similarity index 100% |
969 | 5 | rename from tests/01-functional | 12 | rename from tests/01-functional |
970 | 6 | rename to src/tests/01-functional | 13 | rename to src/tests/01-functional |
971 | diff --git a/tests/functional/conftest.py b/src/tests/functional/conftest.py | |||
972 | 7 | similarity index 75% | 14 | similarity index 75% |
973 | 8 | rename from tests/functional/conftest.py | 15 | rename from tests/functional/conftest.py |
974 | 9 | rename to src/tests/functional/conftest.py | 16 | rename to src/tests/functional/conftest.py |
975 | index aa42a09..fdd4356 100644 | |||
976 | --- a/tests/functional/conftest.py | |||
977 | +++ b/src/tests/functional/conftest.py | |||
978 | @@ -1,4 +1,5 @@ | |||
979 | 1 | #!/usr/bin/python3 | 1 | #!/usr/bin/python3 |
980 | 2 | """Functional tests utilities.""" | ||
981 | 2 | 3 | ||
982 | 3 | import asyncio | 4 | import asyncio |
983 | 4 | import json | 5 | import json |
984 | @@ -16,7 +17,7 @@ import pytest | |||
985 | 16 | STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\"" # noqa: E501 | 17 | STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\"" # noqa: E501 |
986 | 17 | 18 | ||
987 | 18 | 19 | ||
989 | 19 | @pytest.yield_fixture(scope='module') | 20 | @pytest.yield_fixture(scope="module") |
990 | 20 | def event_loop(request): | 21 | def event_loop(request): |
991 | 21 | """Override the default pytest event loop to allow for broaded scopedv fixtures.""" | 22 | """Override the default pytest event loop to allow for broaded scopedv fixtures.""" |
992 | 22 | loop = asyncio.get_event_loop_policy().new_event_loop() | 23 | loop = asyncio.get_event_loop_policy().new_event_loop() |
993 | @@ -27,7 +28,7 @@ def event_loop(request): | |||
994 | 27 | asyncio.set_event_loop(None) | 28 | asyncio.set_event_loop(None) |
995 | 28 | 29 | ||
996 | 29 | 30 | ||
998 | 30 | @pytest.fixture(scope='module') | 31 | @pytest.fixture(scope="module") |
999 | 31 | async def controller(): | 32 | async def controller(): |
1000 | 32 | """Connect to the current controller.""" | 33 | """Connect to the current controller.""" |
1001 | 33 | controller = Controller() | 34 | controller = Controller() |
1002 | @@ -36,21 +37,21 @@ async def controller(): | |||
1003 | 36 | await controller.disconnect() | 37 | await controller.disconnect() |
1004 | 37 | 38 | ||
1005 | 38 | 39 | ||
1007 | 39 | @pytest.fixture(scope='module') | 40 | @pytest.fixture(scope="module") |
1008 | 40 | async def model(controller): | 41 | async def model(controller): |
1009 | 41 | """Create a model that lives only for the duration of the test.""" | 42 | """Create a model that lives only for the duration of the test.""" |
1010 | 42 | model_name = "functest-{}".format(uuid.uuid4()) | 43 | model_name = "functest-{}".format(uuid.uuid4()) |
1011 | 43 | model = await controller.add_model(model_name) | 44 | model = await controller.add_model(model_name) |
1012 | 44 | yield model | 45 | yield model |
1013 | 45 | await model.disconnect() | 46 | await model.disconnect() |
1015 | 46 | if os.getenv('test_preserve_model'): | 47 | if os.getenv("PYTEST_KEEP_MODEL"): |
1016 | 47 | return | 48 | return |
1017 | 48 | await controller.destroy_model(model_name) | 49 | await controller.destroy_model(model_name) |
1018 | 49 | while model_name in await controller.list_models(): | 50 | while model_name in await controller.list_models(): |
1019 | 50 | await asyncio.sleep(1) | 51 | await asyncio.sleep(1) |
1020 | 51 | 52 | ||
1021 | 52 | 53 | ||
1023 | 53 | @pytest.fixture(scope='module') | 54 | @pytest.fixture(scope="module") |
1024 | 54 | async def current_model(): | 55 | async def current_model(): |
1025 | 55 | """Return the current model, does not create or destroy it.""" | 56 | """Return the current model, does not create or destroy it.""" |
1026 | 56 | model = Model() | 57 | model = Model() |
1027 | @@ -60,31 +61,36 @@ async def current_model(): | |||
1028 | 60 | 61 | ||
1029 | 61 | 62 | ||
1030 | 62 | @pytest.fixture | 63 | @pytest.fixture |
1032 | 63 | async def get_app(model): | 64 | async def get_app(model): # noqa: D202 |
1033 | 64 | """Return the application requested.""" | 65 | """Return the application requested.""" |
1034 | 66 | |||
1035 | 65 | async def _get_app(name): | 67 | async def _get_app(name): |
1036 | 66 | try: | 68 | try: |
1037 | 67 | return model.applications[name] | 69 | return model.applications[name] |
1038 | 68 | except KeyError: | 70 | except KeyError: |
1039 | 69 | raise JujuError("Cannot find application {}".format(name)) | 71 | raise JujuError("Cannot find application {}".format(name)) |
1040 | 72 | |||
1041 | 70 | return _get_app | 73 | return _get_app |
1042 | 71 | 74 | ||
1043 | 72 | 75 | ||
1044 | 73 | @pytest.fixture | 76 | @pytest.fixture |
1046 | 74 | async def get_unit(model): | 77 | async def get_unit(model): # noqa: D202 |
1047 | 75 | """Return the requested <app_name>/<unit_number> unit.""" | 78 | """Return the requested <app_name>/<unit_number> unit.""" |
1048 | 79 | |||
1049 | 76 | async def _get_unit(name): | 80 | async def _get_unit(name): |
1050 | 77 | try: | 81 | try: |
1052 | 78 | (app_name, unit_number) = name.split('/') | 82 | (app_name, unit_number) = name.split("/") |
1053 | 79 | return model.applications[app_name].units[unit_number] | 83 | return model.applications[app_name].units[unit_number] |
1054 | 80 | except (KeyError, ValueError): | 84 | except (KeyError, ValueError): |
1055 | 81 | raise JujuError("Cannot find unit {}".format(name)) | 85 | raise JujuError("Cannot find unit {}".format(name)) |
1056 | 86 | |||
1057 | 82 | return _get_unit | 87 | return _get_unit |
1058 | 83 | 88 | ||
1059 | 84 | 89 | ||
1060 | 85 | @pytest.fixture | 90 | @pytest.fixture |
1062 | 86 | async def get_entity(model, get_unit, get_app): | 91 | async def get_entity(model, get_unit, get_app): # noqa: D202 |
1063 | 87 | """Return a unit or an application.""" | 92 | """Return a unit or an application.""" |
1064 | 93 | |||
1065 | 88 | async def _get_entity(name): | 94 | async def _get_entity(name): |
1066 | 89 | try: | 95 | try: |
1067 | 90 | return await get_unit(name) | 96 | return await get_unit(name) |
1068 | @@ -93,61 +99,65 @@ async def get_entity(model, get_unit, get_app): | |||
1069 | 93 | return await get_app(name) | 99 | return await get_app(name) |
1070 | 94 | except JujuError: | 100 | except JujuError: |
1071 | 95 | raise JujuError("Cannot find entity {}".format(name)) | 101 | raise JujuError("Cannot find entity {}".format(name)) |
1072 | 102 | |||
1073 | 96 | return _get_entity | 103 | return _get_entity |
1074 | 97 | 104 | ||
1075 | 98 | 105 | ||
1076 | 99 | @pytest.fixture | 106 | @pytest.fixture |
1080 | 100 | async def run_command(get_unit): | 107 | async def run_command(get_unit): # noqa: D202 |
1081 | 101 | """ | 108 | """Run a command on an unit.""" |
1079 | 102 | Run a command on a unit. | ||
1082 | 103 | 109 | ||
1083 | 104 | :param cmd: Command to be run | ||
1084 | 105 | :param target: Unit object or unit name string | ||
1085 | 106 | """ | ||
1086 | 107 | async def _run_command(cmd, target): | 110 | async def _run_command(cmd, target): |
1092 | 108 | unit = ( | 111 | """Run a command on a unit. |
1093 | 109 | target | 112 | |
1094 | 110 | if type(target) is juju.unit.Unit | 113 | :param cmd: Command to be run |
1095 | 111 | else await get_unit(target) | 114 | :param target: Unit object or unit name string |
1096 | 112 | ) | 115 | """ |
1097 | 116 | unit = target if type(target) is juju.unit.Unit else await get_unit(target) | ||
1098 | 113 | action = await unit.run(cmd) | 117 | action = await unit.run(cmd) |
1099 | 114 | return action.results | 118 | return action.results |
1100 | 119 | |||
1101 | 115 | return _run_command | 120 | return _run_command |
1102 | 116 | 121 | ||
1103 | 117 | 122 | ||
1104 | 118 | @pytest.fixture | 123 | @pytest.fixture |
1108 | 119 | async def file_stat(run_command): | 124 | async def file_stat(run_command): # noqa: D202 |
1109 | 120 | """ | 125 | """Get file stat from an unit.""" |
1107 | 121 | Run stat on a file. | ||
1110 | 122 | 126 | ||
1111 | 123 | :param path: File path | ||
1112 | 124 | :param target: Unit object or unit name string | ||
1113 | 125 | """ | ||
1114 | 126 | async def _file_stat(path, target): | 127 | async def _file_stat(path, target): |
1115 | 128 | """Run stat on a file. | ||
1116 | 129 | |||
1117 | 130 | :param path: File path | ||
1118 | 131 | :param target: Unit object or unit name string | ||
1119 | 132 | """ | ||
1120 | 127 | cmd = STAT_FILE % path | 133 | cmd = STAT_FILE % path |
1121 | 128 | results = await run_command(cmd, target) | 134 | results = await run_command(cmd, target) |
1123 | 129 | return json.loads(results['Stdout']) | 135 | return json.loads(results["Stdout"]) |
1124 | 136 | |||
1125 | 130 | return _file_stat | 137 | return _file_stat |
1126 | 131 | 138 | ||
1127 | 132 | 139 | ||
1128 | 133 | @pytest.fixture | 140 | @pytest.fixture |
1132 | 134 | async def file_contents(run_command): | 141 | async def file_contents(run_command): # noqa: D202 |
1133 | 135 | """ | 142 | """Return the contents of a file.""" |
1131 | 136 | Return the contents of a file. | ||
1134 | 137 | 143 | ||
1135 | 138 | :param path: File path | ||
1136 | 139 | :param target: Unit object or unit name string | ||
1137 | 140 | """ | ||
1138 | 141 | async def _file_contents(path, target): | 144 | async def _file_contents(path, target): |
1140 | 142 | cmd = 'cat {}'.format(path) | 145 | """Return the contents of a file. |
1141 | 146 | |||
1142 | 147 | :param path: File path | ||
1143 | 148 | :param target: Unit object or unit name string | ||
1144 | 149 | """ | ||
1145 | 150 | cmd = "cat {}".format(path) | ||
1146 | 143 | results = await run_command(cmd, target) | 151 | results = await run_command(cmd, target) |
1148 | 144 | return results['Stdout'] | 152 | return results["Stdout"] |
1149 | 153 | |||
1150 | 145 | return _file_contents | 154 | return _file_contents |
1151 | 146 | 155 | ||
1152 | 147 | 156 | ||
1153 | 148 | @pytest.fixture | 157 | @pytest.fixture |
1155 | 149 | async def reconfigure_app(get_app, model): | 158 | async def reconfigure_app(get_app, model): # noqa: D202 |
1156 | 150 | """Apply a different config to the requested app.""" | 159 | """Apply a different config to the requested app.""" |
1157 | 160 | |||
1158 | 151 | async def _reconfigure_app(cfg, target): | 161 | async def _reconfigure_app(cfg, target): |
1159 | 152 | application = ( | 162 | application = ( |
1160 | 153 | target | 163 | target |
1161 | @@ -156,14 +166,17 @@ async def reconfigure_app(get_app, model): | |||
1162 | 156 | ) | 166 | ) |
1163 | 157 | await application.set_config(cfg) | 167 | await application.set_config(cfg) |
1164 | 158 | await application.get_config() | 168 | await application.get_config() |
1166 | 159 | await model.block_until(lambda: application.status == 'active') | 169 | await model.block_until(lambda: application.status == "active") |
1167 | 170 | |||
1168 | 160 | return _reconfigure_app | 171 | return _reconfigure_app |
1169 | 161 | 172 | ||
1170 | 162 | 173 | ||
1171 | 163 | @pytest.fixture | 174 | @pytest.fixture |
1173 | 164 | async def create_group(run_command): | 175 | async def create_group(run_command): # noqa: D202 |
1174 | 165 | """Create the UNIX group specified.""" | 176 | """Create the UNIX group specified.""" |
1175 | 177 | |||
1176 | 166 | async def _create_group(group_name, target): | 178 | async def _create_group(group_name, target): |
1177 | 167 | cmd = "sudo groupadd %s" % group_name | 179 | cmd = "sudo groupadd %s" % group_name |
1178 | 168 | await run_command(cmd, target) | 180 | await run_command(cmd, target) |
1179 | 181 | |||
1180 | 169 | return _create_group | 182 | return _create_group |
1181 | diff --git a/tests/functional/requirements.txt b/src/tests/functional/requirements.txt | |||
1182 | 170 | similarity index 100% | 183 | similarity index 100% |
1183 | 171 | rename from tests/functional/requirements.txt | 184 | rename from tests/functional/requirements.txt |
1184 | 172 | rename to src/tests/functional/requirements.txt | 185 | rename to src/tests/functional/requirements.txt |
1185 | diff --git a/tests/functional/test_deploy.py b/src/tests/functional/test_deploy.py | |||
1186 | 173 | similarity index 51% | 186 | similarity index 51% |
1187 | 174 | rename from tests/functional/test_deploy.py | 187 | rename from tests/functional/test_deploy.py |
1188 | 175 | rename to src/tests/functional/test_deploy.py | 188 | rename to src/tests/functional/test_deploy.py |
1189 | index cdb5ae4..11e8297 100644 | |||
1190 | --- a/tests/functional/test_deploy.py | |||
1191 | +++ b/src/tests/functional/test_deploy.py | |||
1192 | @@ -1,4 +1,5 @@ | |||
1193 | 1 | #!/usr/bin/python3.6 | 1 | #!/usr/bin/python3.6 |
1194 | 2 | """Charm functional tests.""" | ||
1195 | 2 | 3 | ||
1196 | 3 | import os | 4 | import os |
1197 | 4 | 5 | ||
1198 | @@ -6,14 +7,16 @@ import pytest | |||
1199 | 6 | 7 | ||
1200 | 7 | pytestmark = pytest.mark.asyncio | 8 | pytestmark = pytest.mark.asyncio |
1201 | 8 | 9 | ||
1203 | 9 | charm_build_dir = os.getenv('CHARM_BUILD_DIR', '..').rstrip('/') | 10 | charm_build_dir = os.getenv("CHARM_BUILD_DIR").rstrip("/") |
1204 | 10 | 11 | ||
1205 | 11 | 12 | ||
1207 | 12 | sources = [('local', '{}/builds/sudo-pair'.format(charm_build_dir))] | 13 | sources = [("local", "{}/sudo-pair".format(charm_build_dir))] |
1208 | 13 | 14 | ||
1212 | 14 | series = ['xenial', | 15 | series = [ |
1213 | 15 | 'bionic', | 16 | "xenial", |
1214 | 16 | ] | 17 | "bionic", |
1215 | 18 | "focal", | ||
1216 | 19 | ] | ||
1217 | 17 | 20 | ||
1218 | 18 | 21 | ||
1219 | 19 | ############ | 22 | ############ |
1220 | @@ -36,8 +39,8 @@ def source(request): | |||
1221 | 36 | @pytest.fixture | 39 | @pytest.fixture |
1222 | 37 | async def app(model, series, source): | 40 | async def app(model, series, source): |
1223 | 38 | """Return application of the charm under test.""" | 41 | """Return application of the charm under test.""" |
1226 | 39 | app_name = 'sudo-pair-{}'.format(series) | 42 | app_name = "sudo-pair-{}".format(series) |
1227 | 40 | return await model._wait_for_new('application', app_name) | 43 | return await model._wait_for_new("application", app_name) |
1228 | 41 | 44 | ||
1229 | 42 | 45 | ||
1230 | 43 | @pytest.fixture | 46 | @pytest.fixture |
1231 | @@ -50,108 +53,92 @@ async def unit(app): | |||
1232 | 50 | # TESTS # | 53 | # TESTS # |
1233 | 51 | ######### | 54 | ######### |
1234 | 52 | 55 | ||
1235 | 56 | |||
1236 | 53 | async def test_deploy_app(model, series, source): | 57 | async def test_deploy_app(model, series, source): |
1237 | 54 | """Deploy the sudo_pair app as a subordinate of ubuntu.""" | 58 | """Deploy the sudo_pair app as a subordinate of ubuntu.""" |
1238 | 55 | await model.deploy( | 59 | await model.deploy( |
1243 | 56 | 'ubuntu', | 60 | "ubuntu", application_name="ubuntu-" + series, series=series, channel="stable" |
1240 | 57 | application_name='ubuntu-' + series, | ||
1241 | 58 | series=series, | ||
1242 | 59 | channel='stable' | ||
1244 | 60 | ) | 61 | ) |
1245 | 61 | sudo_pair_app = await model.deploy( | 62 | sudo_pair_app = await model.deploy( |
1246 | 62 | source[1], | 63 | source[1], |
1248 | 63 | application_name='sudo-pair-' + series, | 64 | application_name="sudo-pair-" + series, |
1249 | 64 | series=series, | 65 | series=series, |
1250 | 65 | num_units=0, | 66 | num_units=0, |
1251 | 66 | config={ | 67 | config={ |
1256 | 67 | 'bypass_cmds': '/bin/ls', | 68 | "bypass_cmds": "/bin/ls", |
1257 | 68 | 'groups_enforced': 'ubuntu', | 69 | "groups_enforced": "ubuntu", |
1258 | 69 | 'bypass_group': 'warthogs', | 70 | "bypass_group": "warthogs", |
1259 | 70 | } | 71 | }, |
1260 | 71 | ) | 72 | ) |
1261 | 72 | await model.add_relation( | 73 | await model.add_relation( |
1264 | 73 | 'ubuntu-{}:juju-info'.format(series), | 74 | "ubuntu-{}:juju-info".format(series), "sudo-pair-{}:juju-info".format(series) |
1265 | 74 | 'sudo-pair-{}:juju-info'.format(series)) | 75 | ) |
1266 | 75 | 76 | ||
1268 | 76 | await model.block_until(lambda: sudo_pair_app.status == 'active') | 77 | await model.block_until(lambda: sudo_pair_app.status == "active") |
1269 | 77 | # no need to cleanup since the model will be be torn down at the end of the | 78 | # no need to cleanup since the model will be be torn down at the end of the |
1270 | 78 | # testing | 79 | # testing |
1271 | 79 | 80 | ||
1272 | 80 | 81 | ||
1273 | 81 | async def test_status(app): | 82 | async def test_status(app): |
1274 | 82 | """Check that the app is in active state.""" | 83 | """Check that the app is in active state.""" |
1299 | 83 | assert app.status == 'active' | 84 | assert app.status == "active" |
1300 | 84 | 85 | ||
1301 | 85 | 86 | ||
1302 | 86 | @pytest.mark.parametrize("path,expected_stat", [ | 87 | @pytest.mark.parametrize( |
1303 | 87 | ('/usr/lib/sudo/sudo_pair.so', { | 88 | "path,expected_stat", |
1304 | 88 | 'gid': 0, | 89 | [ |
1305 | 89 | 'uid': 0, | 90 | ("/usr/lib/sudo/sudo_pair.so", {"gid": 0, "uid": 0, "mode": "0o100644"}), |
1306 | 90 | 'mode': '0o100644'}), | 91 | ("/usr/bin/sudo_approve", {"gid": 0, "uid": 0, "mode": "0o100755"}), |
1307 | 91 | ('/usr/bin/sudo_approve', { | 92 | ("/etc/sudo_pair.prompt.user", {"gid": 0, "uid": 0, "mode": "0o100644"}), |
1308 | 92 | 'gid': 0, | 93 | ("/etc/sudo_pair.prompt.pair", {"gid": 0, "uid": 0, "mode": "0o100644"}), |
1309 | 93 | 'uid': 0, | 94 | ("/var/run/sudo_pair", {"gid": 0, "uid": 0, "mode": "0o40644"}), |
1310 | 94 | 'mode': '0o100755'}), | 95 | ], |
1311 | 95 | ('/etc/sudo_pair.prompt.user', { | 96 | ) |
1288 | 96 | 'gid': 0, | ||
1289 | 97 | 'uid': 0, | ||
1290 | 98 | 'mode': '0o100644'}), | ||
1291 | 99 | ('/etc/sudo_pair.prompt.pair', { | ||
1292 | 100 | 'gid': 0, | ||
1293 | 101 | 'uid': 0, | ||
1294 | 102 | 'mode': '0o100644'}), | ||
1295 | 103 | ('/var/run/sudo_pair', { | ||
1296 | 104 | 'gid': 0, | ||
1297 | 105 | 'uid': 0, | ||
1298 | 106 | 'mode': '0o40644'})]) | ||
1312 | 107 | async def test_stats(path, expected_stat, unit, file_stat): | 97 | async def test_stats(path, expected_stat, unit, file_stat): |
1313 | 108 | """Check that created files have the correct permissions.""" | 98 | """Check that created files have the correct permissions.""" |
1314 | 109 | test_stat = await file_stat(path, unit) | 99 | test_stat = await file_stat(path, unit) |
1319 | 110 | assert test_stat['size'] > 0 | 100 | assert test_stat["size"] > 0 |
1320 | 111 | assert test_stat['gid'] == expected_stat['gid'] | 101 | assert test_stat["gid"] == expected_stat["gid"] |
1321 | 112 | assert test_stat['uid'] == expected_stat['uid'] | 102 | assert test_stat["uid"] == expected_stat["uid"] |
1322 | 113 | assert test_stat['mode'] == expected_stat['mode'] | 103 | assert test_stat["mode"] == expected_stat["mode"] |
1323 | 114 | 104 | ||
1324 | 115 | 105 | ||
1325 | 116 | async def test_sudoers(file_contents, unit): | 106 | async def test_sudoers(file_contents, unit): |
1326 | 117 | """Check the content of sudoers file.""" | 107 | """Check the content of sudoers file.""" |
1327 | 118 | sudoers_content = await file_contents("/etc/sudoers", unit) | 108 | sudoers_content = await file_contents("/etc/sudoers", unit) |
1329 | 119 | assert 'Defaults log_output' in sudoers_content | 109 | assert "Defaults log_output" in sudoers_content |
1330 | 120 | 110 | ||
1331 | 121 | 111 | ||
1332 | 122 | async def test_sudoers_bypass_conf(file_contents, unit): | 112 | async def test_sudoers_bypass_conf(file_contents, unit): |
1333 | 123 | """Check the content of sudoers bypass command file.""" | 113 | """Check the content of sudoers bypass command file.""" |
1334 | 124 | path = "/etc/sudoers.d/91-bypass-sudopair-cmds" | 114 | path = "/etc/sudoers.d/91-bypass-sudopair-cmds" |
1338 | 125 | sudoers_bypass_content = await file_contents(path=path, | 115 | sudoers_bypass_content = await file_contents(path=path, target=unit) |
1339 | 126 | target=unit) | 116 | content = "%warthogs ALL = (ALL) NOLOG_OUTPUT: /bin/ls" |
1337 | 127 | content = '%warthogs ALL = (ALL) NOLOG_OUTPUT: /bin/ls' | ||
1340 | 128 | assert content in sudoers_bypass_content | 117 | assert content in sudoers_bypass_content |
1341 | 129 | 118 | ||
1342 | 130 | 119 | ||
1343 | 131 | async def test_reconfigure(reconfigure_app, file_contents, unit, app): | 120 | async def test_reconfigure(reconfigure_app, file_contents, unit, app): |
1350 | 132 | """Change a charm config parameter and verify that it has been propagated to the unit.""" | 121 | """Change a charm config parameter and verify it is applied.""" |
1351 | 133 | sudo_approve_path = '/usr/bin/sudo_approve' | 122 | sudo_approve_path = "/usr/bin/sudo_approve" |
1352 | 134 | await reconfigure_app(cfg={'auto_approve': 'false'}, | 123 | await reconfigure_app(cfg={"auto_approve": "false"}, target=app) |
1353 | 135 | target=app) | 124 | sudo_approve_content = await file_contents(path=sudo_approve_path, target=unit) |
1348 | 136 | sudo_approve_content = await file_contents(path=sudo_approve_path, | ||
1349 | 137 | target=unit) | ||
1354 | 138 | new_content = 'echo "You can\'t approve your own session."' | 125 | new_content = 'echo "You can\'t approve your own session."' |
1355 | 139 | assert new_content in sudo_approve_content | 126 | assert new_content in sudo_approve_content |
1356 | 140 | 127 | ||
1357 | 141 | 128 | ||
1358 | 142 | async def test_remove_relation(app, model, run_command): | 129 | async def test_remove_relation(app, model, run_command): |
1359 | 143 | """Check that the relation is removed.""" | 130 | """Check that the relation is removed.""" |
1363 | 144 | series = app.units[0].data['series'] | 131 | series = app.units[0].data["series"] |
1364 | 145 | app_name = 'sudo-pair-{}'.format(series) | 132 | app_name = "sudo-pair-{}".format(series) |
1365 | 146 | principalname = 'ubuntu-{}'.format(series) | 133 | principalname = "ubuntu-{}".format(series) |
1366 | 147 | await app.remove_relation( | 134 | await app.remove_relation( |
1369 | 148 | '{}:juju-info'.format(app_name), | 135 | "{}:juju-info".format(app_name), "{}:juju-info".format(principalname) |
1370 | 149 | '{}:juju-info'.format(principalname)) | 136 | ) |
1371 | 150 | await model.block_until(lambda: not app.relations) | 137 | await model.block_until(lambda: not app.relations) |
1372 | 151 | principal = model.applications[principalname].units[0] | 138 | principal = model.applications[principalname].units[0] |
1375 | 152 | res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal) | 139 | res = await run_command("test -f /etc/sudo.conf || echo gone", target=principal) |
1376 | 153 | assert res['Stdout'].strip() == 'gone' | 140 | assert res["Stdout"].strip() == "gone" |
1377 | 154 | await model.add_relation( | 141 | await model.add_relation( |
1380 | 155 | '{}:juju-info'.format(principalname), | 142 | "{}:juju-info".format(principalname), "{}:juju-info".format(app_name) |
1381 | 156 | '{}:juju-info'.format(app_name)) | 143 | ) |
1382 | 157 | await model.block_until(lambda: app.relations) | 144 | await model.block_until(lambda: app.relations) |
1383 | diff --git a/tests/tests.yaml b/src/tests/tests.yaml | |||
1384 | 158 | similarity index 100% | 145 | similarity index 100% |
1385 | 159 | rename from tests/tests.yaml | 146 | rename from tests/tests.yaml |
1386 | 160 | rename to src/tests/tests.yaml | 147 | rename to src/tests/tests.yaml |
1387 | diff --git a/tests/unit/conftest.py b/src/tests/unit/conftest.py | |||
1388 | 161 | similarity index 82% | 148 | similarity index 82% |
1389 | 162 | rename from tests/unit/conftest.py | 149 | rename from tests/unit/conftest.py |
1390 | 163 | rename to src/tests/unit/conftest.py | 150 | rename to src/tests/unit/conftest.py |
1391 | index c0800ea..bc65d15 100644 | |||
1392 | --- a/tests/unit/conftest.py | |||
1393 | +++ b/src/tests/unit/conftest.py | |||
1394 | @@ -1,4 +1,5 @@ | |||
1395 | 1 | #!/usr/bin/python3 | 1 | #!/usr/bin/python3 |
1396 | 2 | """Unit tests utilities.""" | ||
1397 | 2 | 3 | ||
1398 | 3 | import grp | 4 | import grp |
1399 | 4 | import os | 5 | import os |
1400 | @@ -10,29 +11,33 @@ import pytest | |||
1401 | 10 | 11 | ||
1402 | 11 | @pytest.fixture | 12 | @pytest.fixture |
1403 | 12 | def mock_hookenv_config(monkeypatch): | 13 | def mock_hookenv_config(monkeypatch): |
1404 | 14 | """Mock hookenv config.""" | ||
1405 | 13 | import yaml | 15 | import yaml |
1406 | 14 | 16 | ||
1407 | 15 | def mock_config(): | 17 | def mock_config(): |
1408 | 16 | cfg = {} | 18 | cfg = {} |
1410 | 17 | yml = yaml.load(open('./config.yaml')) | 19 | yml = yaml.safe_load(open("./config.yaml")) |
1411 | 18 | 20 | ||
1412 | 19 | # Load all defaults | 21 | # Load all defaults |
1415 | 20 | for key, value in yml['options'].items(): | 22 | for key, value in yml["options"].items(): |
1416 | 21 | cfg[key] = value['default'] | 23 | cfg[key] = value["default"] |
1417 | 22 | 24 | ||
1418 | 23 | return cfg | 25 | return cfg |
1419 | 24 | 26 | ||
1421 | 25 | monkeypatch.setattr('charmhelpers.core.hookenv.config', mock_config) | 27 | monkeypatch.setattr("charmhelpers.core.hookenv.config", mock_config) |
1422 | 26 | 28 | ||
1423 | 27 | 29 | ||
1424 | 28 | @pytest.fixture | 30 | @pytest.fixture |
1425 | 29 | def mock_charm_dir(monkeypatch): | 31 | def mock_charm_dir(monkeypatch): |
1427 | 30 | monkeypatch.setattr('charmhelpers.core.hookenv.charm_dir', lambda: '.') | 32 | """Mock charm dir.""" |
1428 | 33 | monkeypatch.setattr("charmhelpers.core.hookenv.charm_dir", lambda: ".") | ||
1429 | 31 | 34 | ||
1430 | 32 | 35 | ||
1431 | 33 | @pytest.fixture | 36 | @pytest.fixture |
1432 | 34 | def sph(mock_hookenv_config, mock_charm_dir, tmpdir): | 37 | def sph(mock_hookenv_config, mock_charm_dir, tmpdir): |
1433 | 38 | """Return SudoPairHelpers with mocks.""" | ||
1434 | 35 | from libsudopair import SudoPairHelper | 39 | from libsudopair import SudoPairHelper |
1435 | 40 | |||
1436 | 36 | sph = SudoPairHelper() | 41 | sph = SudoPairHelper() |
1437 | 37 | sph.owner = pwd.getpwuid(os.getuid()).pw_name | 42 | sph.owner = pwd.getpwuid(os.getuid()).pw_name |
1438 | 38 | sph.group = grp.getgrgid(os.getgid()).gr_name | 43 | sph.group = grp.getgrgid(os.getgid()).gr_name |
1439 | @@ -49,4 +54,5 @@ def sph(mock_hookenv_config, mock_charm_dir, tmpdir): | |||
1440 | 49 | sph.sudoers_bypass_path = tmpdir.join(sph.sudoers_bypass_path) | 54 | sph.sudoers_bypass_path = tmpdir.join(sph.sudoers_bypass_path) |
1441 | 50 | sph.socket_dir_perms = 0o775 | 55 | sph.socket_dir_perms = 0o775 |
1442 | 51 | sph.sudo_conf_perms = 0o644 | 56 | sph.sudo_conf_perms = 0o644 |
1443 | 57 | sph.pagerduty_path = tmpdir.join(sph.pagerduty_path) | ||
1444 | 52 | return sph | 58 | return sph |
1445 | diff --git a/tests/unit/requirements.txt b/src/tests/unit/requirements.txt | |||
1446 | 53 | similarity index 100% | 59 | similarity index 100% |
1447 | 54 | rename from tests/unit/requirements.txt | 60 | rename from tests/unit/requirements.txt |
1448 | 55 | rename to src/tests/unit/requirements.txt | 61 | rename to src/tests/unit/requirements.txt |
1449 | diff --git a/src/tests/unit/test_actions.py b/src/tests/unit/test_actions.py | |||
1450 | 56 | new file mode 100644 | 62 | new file mode 100644 |
1451 | index 0000000..fd0758a | |||
1452 | --- /dev/null | |||
1453 | +++ b/src/tests/unit/test_actions.py | |||
1454 | @@ -0,0 +1,23 @@ | |||
1455 | 1 | """Unit tests for charm actions.""" | ||
1456 | 2 | |||
1457 | 3 | import os | ||
1458 | 4 | import sys | ||
1459 | 5 | import unittest.mock as mock | ||
1460 | 6 | |||
1461 | 7 | action_path = os.path.join(os.path.dirname(__file__), "..", "..", "actions") | ||
1462 | 8 | sys.path.append(action_path) | ||
1463 | 9 | import actions # NOQA | ||
1464 | 10 | |||
1465 | 11 | |||
1466 | 12 | @mock.patch("libsudopair.SudoPairHelper") | ||
1467 | 13 | @mock.patch("actions.action_set") | ||
1468 | 14 | @mock.patch("actions.status_set") | ||
1469 | 15 | @mock.patch("actions.subprocess.run") | ||
1470 | 16 | def test_remove_action(subprocess_run, status_set, action_set, sudo_pair_helper): | ||
1471 | 17 | """Verify remove action.""" | ||
1472 | 18 | actions.remove() | ||
1473 | 19 | msg = "Successfully removed sudo-pair config and binaries" | ||
1474 | 20 | action_set.assert_called_with({"message": msg}) | ||
1475 | 21 | status_set.assert_called_with("active", msg) | ||
1476 | 22 | sudo_pair_helper().deconfigure.assert_called() | ||
1477 | 23 | subprocess_run.assert_called_with(["/usr/bin/pagerdutyevent.py", msg]) | ||
1478 | diff --git a/src/tests/unit/test_libsudopair.py b/src/tests/unit/test_libsudopair.py | |||
1479 | 0 | new file mode 100644 | 24 | new file mode 100644 |
1480 | index 0000000..8d730ad | |||
1481 | --- /dev/null | |||
1482 | +++ b/src/tests/unit/test_libsudopair.py | |||
1483 | @@ -0,0 +1,206 @@ | |||
1484 | 1 | """Sudopair lib unit tests.""" | ||
1485 | 2 | |||
1486 | 3 | import filecmp | ||
1487 | 4 | import grp | ||
1488 | 5 | import os | ||
1489 | 6 | |||
1490 | 7 | from libsudopair import check_valid_group, group_id | ||
1491 | 8 | |||
1492 | 9 | |||
1493 | 10 | def test_check_valid_group(): | ||
1494 | 11 | """Check an unix group is valid.""" | ||
1495 | 12 | assert not check_valid_group("fake_group") | ||
1496 | 13 | assert check_valid_group(grp.getgrgid(os.getgid()).gr_name) | ||
1497 | 14 | |||
1498 | 15 | |||
1499 | 16 | def test_group_id(): | ||
1500 | 17 | """Verify group_id() is correct.""" | ||
1501 | 18 | assert group_id(grp.getgrgid(os.getgid()).gr_name) == os.getgid() | ||
1502 | 19 | |||
1503 | 20 | |||
1504 | 21 | class TestSudoPairHelper: | ||
1505 | 22 | """Module to test SudoPairHelper lib.""" | ||
1506 | 23 | |||
1507 | 24 | def test_pytest(self): | ||
1508 | 25 | """Assert testing is carryied using pytest.""" | ||
1509 | 26 | assert True | ||
1510 | 27 | |||
1511 | 28 | def test_sph(self, sph): | ||
1512 | 29 | """See if the sph fixture works to load charm configs.""" | ||
1513 | 30 | assert isinstance(sph.charm_config, dict) | ||
1514 | 31 | |||
1515 | 32 | def test_get_config(self, sph): | ||
1516 | 33 | """Check if config contains all the required entries.""" | ||
1517 | 34 | default_keywords = [ | ||
1518 | 35 | "binary_path", | ||
1519 | 36 | "user_prompt_path", | ||
1520 | 37 | "pair_prompt_path", | ||
1521 | 38 | "socket_dir", | ||
1522 | 39 | "gids_enforced", | ||
1523 | 40 | "gids_exempted", | ||
1524 | 41 | ] | ||
1525 | 42 | config = sph.get_config() | ||
1526 | 43 | for option in default_keywords: | ||
1527 | 44 | assert option in config | ||
1528 | 45 | |||
1529 | 46 | def test_set_charm_config(self, sph): | ||
1530 | 47 | """Set new config.""" | ||
1531 | 48 | charm_config = { | ||
1532 | 49 | "groups_enforced": "root", | ||
1533 | 50 | "groups_exempted": "", | ||
1534 | 51 | "bypass_cmds": "", | ||
1535 | 52 | "bypass_group": "", | ||
1536 | 53 | "auto_approve": True, | ||
1537 | 54 | } | ||
1538 | 55 | |||
1539 | 56 | sph.set_charm_config(charm_config) | ||
1540 | 57 | |||
1541 | 58 | for option in charm_config: | ||
1542 | 59 | assert option in sph.get_config() | ||
1543 | 60 | assert sph.get_config()[option] == charm_config[option] | ||
1544 | 61 | |||
1545 | 62 | def test_render_sudo_conf(self, sph, tmpdir): | ||
1546 | 63 | """Check that sudo.conf is rendered correctly.""" | ||
1547 | 64 | # Default config | ||
1548 | 65 | content = sph.render_sudo_conf() | ||
1549 | 66 | expected_content = ( | ||
1550 | 67 | "Plugin sudo_pair sudo_pair.so binary_path={} " | ||
1551 | 68 | "user_prompt_path={} " | ||
1552 | 69 | "pair_prompt_path={} socket_dir={} gids_enforced={}".format( | ||
1553 | 70 | tmpdir.join("/usr/bin/sudo_approve"), | ||
1554 | 71 | tmpdir.join("/etc/sudo_pair.prompt.user"), | ||
1555 | 72 | tmpdir.join("/etc/sudo_pair.prompt.pair"), | ||
1556 | 73 | tmpdir.join("/var/run/sudo_pair"), | ||
1557 | 74 | "0", | ||
1558 | 75 | ) | ||
1559 | 76 | ) | ||
1560 | 77 | assert expected_content in content | ||
1561 | 78 | |||
1562 | 79 | # Gid exempted | ||
1563 | 80 | groups_exempted = grp.getgrgid(os.getgid()).gr_name | ||
1564 | 81 | charm_config = { | ||
1565 | 82 | "groups_enforced": "root", | ||
1566 | 83 | "groups_exempted": groups_exempted, | ||
1567 | 84 | "bypass_cmds": "", | ||
1568 | 85 | "bypass_group": "", | ||
1569 | 86 | "auto_approve": True, | ||
1570 | 87 | } | ||
1571 | 88 | |||
1572 | 89 | sph.set_charm_config(charm_config) | ||
1573 | 90 | expected_content = ( | ||
1574 | 91 | "Plugin sudo_pair sudo_pair.so binary_path={} user_prompt_path={} " | ||
1575 | 92 | "pair_prompt_path={} socket_dir={} gids_enforced={} " | ||
1576 | 93 | "gids_exempted={}".format( | ||
1577 | 94 | tmpdir.join("/usr/bin/sudo_approve"), | ||
1578 | 95 | tmpdir.join("/etc/sudo_pair.prompt.user"), | ||
1579 | 96 | tmpdir.join("/etc/sudo_pair.prompt.pair"), | ||
1580 | 97 | tmpdir.join("/var/run/sudo_pair"), | ||
1581 | 98 | "0", | ||
1582 | 99 | os.getgid(), | ||
1583 | 100 | ) | ||
1584 | 101 | ) | ||
1585 | 102 | |||
1586 | 103 | content = sph.render_sudo_conf() | ||
1587 | 104 | assert expected_content in content | ||
1588 | 105 | |||
1589 | 106 | # Groups enforced | ||
1590 | 107 | groups_enforced = "root," + grp.getgrgid(os.getgid()).gr_name | ||
1591 | 108 | charm_config = { | ||
1592 | 109 | "groups_enforced": groups_enforced, | ||
1593 | 110 | "groups_exempted": "", | ||
1594 | 111 | "bypass_cmds": "", | ||
1595 | 112 | "bypass_group": "", | ||
1596 | 113 | "auto_approve": True, | ||
1597 | 114 | } | ||
1598 | 115 | sph.set_charm_config(charm_config) | ||
1599 | 116 | expected_content = ( | ||
1600 | 117 | "Plugin sudo_pair sudo_pair.so binary_path={} user_prompt_path={} " | ||
1601 | 118 | "pair_prompt_path={} socket_dir={} gids_enforced={}".format( | ||
1602 | 119 | tmpdir.join("/usr/bin/sudo_approve"), | ||
1603 | 120 | tmpdir.join("/etc/sudo_pair.prompt.user"), | ||
1604 | 121 | tmpdir.join("/etc/sudo_pair.prompt.pair"), | ||
1605 | 122 | tmpdir.join("/var/run/sudo_pair"), | ||
1606 | 123 | "0,{}".format(os.getgid()), | ||
1607 | 124 | ) | ||
1608 | 125 | ) | ||
1609 | 126 | content = sph.render_sudo_conf() | ||
1610 | 127 | assert expected_content in content | ||
1611 | 128 | |||
1612 | 129 | def test_render_bypass_cmds(self, sph): | ||
1613 | 130 | """Check that sudoers file is rendered correctly.""" | ||
1614 | 131 | # Root bypass /bin/ls | ||
1615 | 132 | expected_content = "%root ALL = (ALL) NOLOG_OUTPUT: /bin/ls" | ||
1616 | 133 | charm_config = { | ||
1617 | 134 | "groups_enforced": "root", | ||
1618 | 135 | "groups_exempted": "", | ||
1619 | 136 | "bypass_cmds": "/bin/ls", | ||
1620 | 137 | "bypass_group": "root", | ||
1621 | 138 | "auto_approve": True, | ||
1622 | 139 | } | ||
1623 | 140 | sph.set_charm_config(charm_config) | ||
1624 | 141 | content = sph.render_bypass_cmds() | ||
1625 | 142 | assert expected_content in content | ||
1626 | 143 | |||
1627 | 144 | def test_render_sudo_approve(self, sph, tmpdir): | ||
1628 | 145 | """Check that sudo_approve file is rendered correctly.""" | ||
1629 | 146 | # Auto Approve true | ||
1630 | 147 | expected_content = "logger -p auth.warn $log_line" | ||
1631 | 148 | socket_dir = tmpdir.join("/var/run/sudo_pair") | ||
1632 | 149 | expected_content_socket_dir = 'declare -r SUDO_SOCKET_PATH="{}"'.format( | ||
1633 | 150 | socket_dir | ||
1634 | 151 | ) | ||
1635 | 152 | content = sph.render_sudo_approve() | ||
1636 | 153 | assert expected_content in content | ||
1637 | 154 | assert expected_content_socket_dir in content | ||
1638 | 155 | |||
1639 | 156 | # Auto Approve false | ||
1640 | 157 | expected_content = 'echo "You can\'t approve your own session."' | ||
1641 | 158 | charm_config = { | ||
1642 | 159 | "groups_enforced": "root", | ||
1643 | 160 | "groups_exempted": "", | ||
1644 | 161 | "bypass_cmds": "/bin/ls", | ||
1645 | 162 | "bypass_group": "root", | ||
1646 | 163 | "auto_approve": False, | ||
1647 | 164 | } | ||
1648 | 165 | sph.set_charm_config(charm_config) | ||
1649 | 166 | content = sph.render_sudo_approve() | ||
1650 | 167 | assert expected_content in content | ||
1651 | 168 | |||
1652 | 169 | def test_create_socket_dir(self, sph, tmpdir): | ||
1653 | 170 | """Check that sudo_pair socket dir exists.""" | ||
1654 | 171 | sph.create_socket_dir() | ||
1655 | 172 | assert os.path.exists(tmpdir.join("/var/run/sudo_pair")) | ||
1656 | 173 | |||
1657 | 174 | def test_create_tmpfiles_conf(self, sph, tmpdir): | ||
1658 | 175 | """Check that sudo pair temporary conf is rendered correctly.""" | ||
1659 | 176 | sph.create_tmpfiles_conf() | ||
1660 | 177 | expected_content = "d {} 0755 - - -\n".format(sph.socket_dir) | ||
1661 | 178 | with open(tmpdir.join("/usr/lib/tmpfiles.d/sudo_pair.conf")) as f: | ||
1662 | 179 | content = f.read() | ||
1663 | 180 | assert expected_content in content | ||
1664 | 181 | |||
1665 | 182 | def test_install_sudo_pair_so(self, sph, tmpdir): | ||
1666 | 183 | """Check that sudo system lib exists.""" | ||
1667 | 184 | sph.install_sudo_pair_so() | ||
1668 | 185 | assert filecmp.cmp( | ||
1669 | 186 | "./files/sudo_pair.so", tmpdir.join("/usr/lib/sudo/sudo_pair.so") | ||
1670 | 187 | ) | ||
1671 | 188 | |||
1672 | 189 | def test_copy_user_prompt(self, sph, tmpdir): | ||
1673 | 190 | """Check that user prompt exists.""" | ||
1674 | 191 | sph.copy_user_prompt() | ||
1675 | 192 | assert filecmp.cmp( | ||
1676 | 193 | "./files/sudo.prompt.user", tmpdir.join("/etc/sudo_pair.prompt.user") | ||
1677 | 194 | ) | ||
1678 | 195 | |||
1679 | 196 | def test_copy_pair_prompt(self, sph, tmpdir): | ||
1680 | 197 | """Check that pair prompt exists.""" | ||
1681 | 198 | sph.copy_pair_prompt() | ||
1682 | 199 | assert filecmp.cmp( | ||
1683 | 200 | "./files/sudo.prompt.pair", tmpdir.join("/etc/sudo_pair.prompt.pair") | ||
1684 | 201 | ) | ||
1685 | 202 | |||
1686 | 203 | def test_copy_sudoers(self, sph, tmpdir): | ||
1687 | 204 | """Check that sudoers file exists.""" | ||
1688 | 205 | sph.copy_sudoers() | ||
1689 | 206 | assert filecmp.cmp("./files/sudoers", tmpdir.join("/etc/sudoers")) | ||
1690 | diff --git a/tox.ini b/src/tox.ini | |||
1691 | 0 | similarity index 66% | 207 | similarity index 66% |
1692 | 1 | rename from tox.ini | 208 | rename from tox.ini |
1693 | 2 | rename to src/tox.ini | 209 | rename to src/tox.ini |
1694 | index fc364de..2900ead 100644 | |||
1695 | --- a/tox.ini | |||
1696 | +++ b/src/tox.ini | |||
1697 | @@ -1,40 +1,33 @@ | |||
1698 | 1 | [tox] | 1 | [tox] |
1699 | 2 | skipsdist=True | 2 | skipsdist=True |
1700 | 3 | envlist = unit, functional | ||
1701 | 4 | skip_missing_interpreters = True | 3 | skip_missing_interpreters = True |
1702 | 4 | envlist = lint, unit, func | ||
1703 | 5 | 5 | ||
1704 | 6 | [testenv] | 6 | [testenv] |
1705 | 7 | basepython = python3 | 7 | basepython = python3 |
1706 | 8 | setenv = | 8 | setenv = |
1722 | 9 | PYTHONPATH = . | 9 | PYTHONPATH = {toxinidir}:{toxinidir}/lib/:{toxinidir}/hooks/ |
1708 | 10 | |||
1709 | 11 | [testenv:unit] | ||
1710 | 12 | commands = pytest -v --ignore {toxinidir}/tests/functional \ | ||
1711 | 13 | --cov=lib \ | ||
1712 | 14 | --cov=reactive \ | ||
1713 | 15 | --cov=actions \ | ||
1714 | 16 | --cov-report=term \ | ||
1715 | 17 | --cov-report=annotate:report/annotated \ | ||
1716 | 18 | --cov-report=html:report/html | ||
1717 | 19 | deps = -r{toxinidir}/tests/unit/requirements.txt | ||
1718 | 20 | |||
1719 | 21 | setenv = PYTHONPATH={toxinidir}/lib | ||
1720 | 22 | |||
1721 | 23 | [testenv:functional] | ||
1723 | 24 | passenv = | 10 | passenv = |
1724 | 25 | HOME | 11 | HOME |
1725 | 26 | CHARM_BUILD_DIR | ||
1726 | 27 | PATH | 12 | PATH |
1727 | 13 | CHARM_BUILD_DIR | ||
1728 | 28 | PYTEST_KEEP_MODEL | 14 | PYTEST_KEEP_MODEL |
1729 | 29 | PYTEST_CLOUD_NAME | 15 | PYTEST_CLOUD_NAME |
1730 | 30 | PYTEST_CLOUD_REGION | 16 | PYTEST_CLOUD_REGION |
1734 | 31 | commands = pytest -v --ignore {toxinidir}/tests/unit | 17 | PYTEST_MODEL |
1735 | 32 | deps = -r{toxinidir}/tests/functional/requirements.txt | 18 | MODEL_SETTINGS |
1736 | 33 | 19 | HTTP_PROXY | |
1737 | 20 | HTTPS_PROXY | ||
1738 | 21 | NO_PROXY | ||
1739 | 22 | SNAP_HTTP_PROXY | ||
1740 | 23 | SNAP_HTTPS_PROXY | ||
1741 | 34 | 24 | ||
1742 | 35 | [testenv:lint] | 25 | [testenv:lint] |
1744 | 36 | commands = flake8 | 26 | commands = |
1745 | 27 | flake8 | ||
1746 | 28 | black --check --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" . | ||
1747 | 37 | deps = | 29 | deps = |
1748 | 30 | black | ||
1749 | 38 | flake8 | 31 | flake8 |
1750 | 39 | flake8-docstrings | 32 | flake8-docstrings |
1751 | 40 | flake8-import-order | 33 | flake8-import-order |
1752 | @@ -42,10 +35,36 @@ deps = | |||
1753 | 42 | flake8-colors | 35 | flake8-colors |
1754 | 43 | 36 | ||
1755 | 44 | [flake8] | 37 | [flake8] |
1756 | 45 | ignore = D100,D103 # Missing docstring in public module/function | ||
1757 | 46 | exclude = | 38 | exclude = |
1758 | 47 | .git, | 39 | .git, |
1759 | 48 | __pycache__, | 40 | __pycache__, |
1760 | 49 | .tox, | 41 | .tox, |
1762 | 50 | max-line-length = 120 | 42 | charmhelpers, |
1763 | 43 | mod, | ||
1764 | 44 | .build | ||
1765 | 45 | |||
1766 | 46 | max-line-length = 88 | ||
1767 | 51 | max-complexity = 10 | 47 | max-complexity = 10 |
1768 | 48 | |||
1769 | 49 | [testenv:black] | ||
1770 | 50 | commands = | ||
1771 | 51 | black --exclude "/(\.eggs|\.git|\.tox|\.venv|\.build|dist|charmhelpers|mod)/" . | ||
1772 | 52 | deps = | ||
1773 | 53 | black | ||
1774 | 54 | |||
1775 | 55 | [testenv:unit] | ||
1776 | 56 | commands = | ||
1777 | 57 | pytest -v --ignore {toxinidir}/tests/functional \ | ||
1778 | 58 | --cov=lib \ | ||
1779 | 59 | --cov=reactive \ | ||
1780 | 60 | --cov=actions \ | ||
1781 | 61 | --cov=hooks \ | ||
1782 | 62 | --cov=src \ | ||
1783 | 63 | --cov-report=term \ | ||
1784 | 64 | --cov-report=annotate:report/annotated \ | ||
1785 | 65 | --cov-report=html:report/html | ||
1786 | 66 | deps = -r{toxinidir}/tests/unit/requirements.txt | ||
1787 | 67 | |||
1788 | 68 | [testenv:func] | ||
1789 | 69 | commands = pytest -v --ignore {toxinidir}/tests/unit | ||
1790 | 70 | deps = -r{toxinidir}/tests/functional/requirements.txt | ||
1791 | diff --git a/tests/unit/test_libsudopair.py b/tests/unit/test_libsudopair.py | |||
1792 | 52 | deleted file mode 100644 | 71 | deleted file mode 100644 |
1793 | index 3a598af..0000000 | |||
1794 | --- a/tests/unit/test_libsudopair.py | |||
1795 | +++ /dev/null | |||
1796 | @@ -1,185 +0,0 @@ | |||
1797 | 1 | import filecmp | ||
1798 | 2 | import grp | ||
1799 | 3 | import os | ||
1800 | 4 | |||
1801 | 5 | from libsudopair import ( | ||
1802 | 6 | check_valid_group, | ||
1803 | 7 | group_id | ||
1804 | 8 | ) | ||
1805 | 9 | |||
1806 | 10 | |||
1807 | 11 | def test_check_valid_group(): | ||
1808 | 12 | assert not check_valid_group('fake_group') | ||
1809 | 13 | assert check_valid_group(grp.getgrgid(os.getgid()).gr_name) | ||
1810 | 14 | |||
1811 | 15 | |||
1812 | 16 | def test_group_id(): | ||
1813 | 17 | assert group_id(grp.getgrgid(os.getgid()).gr_name) == os.getgid() | ||
1814 | 18 | |||
1815 | 19 | |||
1816 | 20 | class TestSudoPairHelper(): | ||
1817 | 21 | """Module to test SudoPairHelper lib.""" | ||
1818 | 22 | |||
1819 | 23 | def test_pytest(self): | ||
1820 | 24 | """Assert testing is carryied using pytest.""" | ||
1821 | 25 | assert True | ||
1822 | 26 | |||
1823 | 27 | def test_sph(self, sph): | ||
1824 | 28 | """See if the sph fixture works to load charm configs.""" | ||
1825 | 29 | assert isinstance(sph.charm_config, dict) | ||
1826 | 30 | |||
1827 | 31 | def test_get_config(self, sph): | ||
1828 | 32 | """Check if config contains all the required entries.""" | ||
1829 | 33 | default_keywords = [ | ||
1830 | 34 | 'binary_path', | ||
1831 | 35 | 'user_prompt_path', | ||
1832 | 36 | 'pair_prompt_path', | ||
1833 | 37 | 'socket_dir', | ||
1834 | 38 | 'gids_enforced', | ||
1835 | 39 | 'gids_exempted', | ||
1836 | 40 | ] | ||
1837 | 41 | config = sph.get_config() | ||
1838 | 42 | for option in default_keywords: | ||
1839 | 43 | assert option in config | ||
1840 | 44 | |||
1841 | 45 | def test_set_charm_config(self, sph): | ||
1842 | 46 | """Set new config.""" | ||
1843 | 47 | charm_config = { | ||
1844 | 48 | 'groups_enforced': 'root', | ||
1845 | 49 | 'groups_exempted': '', | ||
1846 | 50 | 'bypass_cmds': '', | ||
1847 | 51 | 'bypass_group': '', | ||
1848 | 52 | 'auto_approve': True | ||
1849 | 53 | } | ||
1850 | 54 | |||
1851 | 55 | sph.set_charm_config(charm_config) | ||
1852 | 56 | |||
1853 | 57 | for option in charm_config: | ||
1854 | 58 | assert option in sph.get_config() | ||
1855 | 59 | assert sph.get_config()[option] == charm_config[option] | ||
1856 | 60 | |||
1857 | 61 | def test_render_sudo_conf(self, sph, tmpdir): | ||
1858 | 62 | """Check that sudo.conf is rendered correctly.""" | ||
1859 | 63 | # Default config | ||
1860 | 64 | content = sph.render_sudo_conf() | ||
1861 | 65 | expected_content = 'Plugin sudo_pair sudo_pair.so binary_path={} ' \ | ||
1862 | 66 | 'user_prompt_path={} ' \ | ||
1863 | 67 | 'pair_prompt_path={} socket_dir={} gids_enforced={}'.format( | ||
1864 | 68 | tmpdir.join('/usr/bin/sudo_approve'), | ||
1865 | 69 | tmpdir.join('/etc/sudo_pair.prompt.user'), | ||
1866 | 70 | tmpdir.join('/etc/sudo_pair.prompt.pair'), | ||
1867 | 71 | tmpdir.join('/var/run/sudo_pair'), | ||
1868 | 72 | '0') | ||
1869 | 73 | assert expected_content in content | ||
1870 | 74 | |||
1871 | 75 | # Gid exempted | ||
1872 | 76 | groups_exempted = grp.getgrgid(os.getgid()).gr_name | ||
1873 | 77 | charm_config = { | ||
1874 | 78 | 'groups_enforced': 'root', | ||
1875 | 79 | 'groups_exempted': groups_exempted, | ||
1876 | 80 | 'bypass_cmds': '', | ||
1877 | 81 | 'bypass_group': '', | ||
1878 | 82 | 'auto_approve': True | ||
1879 | 83 | } | ||
1880 | 84 | |||
1881 | 85 | sph.set_charm_config(charm_config) | ||
1882 | 86 | expected_content = \ | ||
1883 | 87 | 'Plugin sudo_pair sudo_pair.so binary_path={} user_prompt_path={} ' \ | ||
1884 | 88 | 'pair_prompt_path={} socket_dir={} gids_enforced={} gids_exempted={}'.format( | ||
1885 | 89 | tmpdir.join('/usr/bin/sudo_approve'), | ||
1886 | 90 | tmpdir.join('/etc/sudo_pair.prompt.user'), | ||
1887 | 91 | tmpdir.join('/etc/sudo_pair.prompt.pair'), | ||
1888 | 92 | tmpdir.join('/var/run/sudo_pair'), '0', os.getgid()) | ||
1889 | 93 | |||
1890 | 94 | content = sph.render_sudo_conf() | ||
1891 | 95 | assert expected_content in content | ||
1892 | 96 | |||
1893 | 97 | # Groups enforced | ||
1894 | 98 | groups_enforced = 'root,' + grp.getgrgid(os.getgid()).gr_name | ||
1895 | 99 | charm_config = { | ||
1896 | 100 | 'groups_enforced': groups_enforced, | ||
1897 | 101 | 'groups_exempted': '', | ||
1898 | 102 | 'bypass_cmds': '', | ||
1899 | 103 | 'bypass_group': '', | ||
1900 | 104 | 'auto_approve': True | ||
1901 | 105 | } | ||
1902 | 106 | sph.set_charm_config(charm_config) | ||
1903 | 107 | expected_content = 'Plugin sudo_pair sudo_pair.so binary_path={} user_prompt_path={} ' \ | ||
1904 | 108 | 'pair_prompt_path={} socket_dir={} gids_enforced={}'.format( | ||
1905 | 109 | tmpdir.join('/usr/bin/sudo_approve'), | ||
1906 | 110 | tmpdir.join('/etc/sudo_pair.prompt.user'), | ||
1907 | 111 | tmpdir.join('/etc/sudo_pair.prompt.pair'), | ||
1908 | 112 | tmpdir.join('/var/run/sudo_pair'), '0,{}'.format(os.getgid())) | ||
1909 | 113 | content = sph.render_sudo_conf() | ||
1910 | 114 | assert expected_content in content | ||
1911 | 115 | |||
1912 | 116 | def test_render_bypass_cmds(self, sph): | ||
1913 | 117 | """Check that sudoers file is rendered correctly.""" | ||
1914 | 118 | # Root bypass /bin/ls | ||
1915 | 119 | expected_content = '%root ALL = (ALL) NOLOG_OUTPUT: /bin/ls' | ||
1916 | 120 | charm_config = { | ||
1917 | 121 | 'groups_enforced': 'root', | ||
1918 | 122 | 'groups_exempted': '', | ||
1919 | 123 | 'bypass_cmds': '/bin/ls', | ||
1920 | 124 | 'bypass_group': 'root', | ||
1921 | 125 | 'auto_approve': True | ||
1922 | 126 | } | ||
1923 | 127 | sph.set_charm_config(charm_config) | ||
1924 | 128 | content = sph.render_bypass_cmds() | ||
1925 | 129 | assert expected_content in content | ||
1926 | 130 | |||
1927 | 131 | def test_render_sudo_approve(self, sph, tmpdir): | ||
1928 | 132 | """Check that sudo_approve file is rendered correctly.""" | ||
1929 | 133 | # Auto Approve true | ||
1930 | 134 | expected_content = 'echo ${log_line} >> /var/log/sudo_pair.log' | ||
1931 | 135 | socket_dir = tmpdir.join('/var/run/sudo_pair') | ||
1932 | 136 | expected_content_socket_dir = 'declare -r SUDO_SOCKET_PATH="{}"'.format(socket_dir) | ||
1933 | 137 | content = sph.render_sudo_approve() | ||
1934 | 138 | assert expected_content in content | ||
1935 | 139 | assert expected_content_socket_dir in content | ||
1936 | 140 | |||
1937 | 141 | # Auto Approve false | ||
1938 | 142 | expected_content = 'echo "You can\'t approve your own session."' | ||
1939 | 143 | charm_config = { | ||
1940 | 144 | 'groups_enforced': 'root', | ||
1941 | 145 | 'groups_exempted': '', | ||
1942 | 146 | 'bypass_cmds': '/bin/ls', | ||
1943 | 147 | 'bypass_group': 'root', | ||
1944 | 148 | 'auto_approve': False | ||
1945 | 149 | } | ||
1946 | 150 | sph.set_charm_config(charm_config) | ||
1947 | 151 | content = sph.render_sudo_approve() | ||
1948 | 152 | assert expected_content in content | ||
1949 | 153 | |||
1950 | 154 | def test_create_socket_dir(self, sph, tmpdir): | ||
1951 | 155 | """Check that sudo_pair socket dir exists.""" | ||
1952 | 156 | sph.create_socket_dir() | ||
1953 | 157 | assert os.path.exists(tmpdir.join('/var/run/sudo_pair')) | ||
1954 | 158 | |||
1955 | 159 | def test_create_tmpfiles_conf(self, sph, tmpdir): | ||
1956 | 160 | """Check that sudo pair temporary conf is rendered correctly.""" | ||
1957 | 161 | sph.create_tmpfiles_conf() | ||
1958 | 162 | expected_content = 'd {} 0755 - - -\n'.format(sph.socket_dir) | ||
1959 | 163 | with open(tmpdir.join('/usr/lib/tmpfiles.d/sudo_pair.conf')) as f: | ||
1960 | 164 | content = f.read() | ||
1961 | 165 | assert expected_content in content | ||
1962 | 166 | |||
1963 | 167 | def test_install_sudo_pair_so(self, sph, tmpdir): | ||
1964 | 168 | """Check that sudo system lib exists.""" | ||
1965 | 169 | sph.install_sudo_pair_so() | ||
1966 | 170 | assert filecmp.cmp('./files/sudo_pair.so', tmpdir.join('/usr/lib/sudo/sudo_pair.so')) | ||
1967 | 171 | |||
1968 | 172 | def test_copy_user_prompt(self, sph, tmpdir): | ||
1969 | 173 | """Check that user prompt exists.""" | ||
1970 | 174 | sph.copy_user_prompt() | ||
1971 | 175 | assert filecmp.cmp('./files/sudo.prompt.user', tmpdir.join('/etc/sudo_pair.prompt.user')) | ||
1972 | 176 | |||
1973 | 177 | def test_copy_pair_prompt(self, sph, tmpdir): | ||
1974 | 178 | """Check that pair prompt exists.""" | ||
1975 | 179 | sph.copy_pair_prompt() | ||
1976 | 180 | assert filecmp.cmp('./files/sudo.prompt.pair', tmpdir.join('/etc/sudo_pair.prompt.pair')) | ||
1977 | 181 | |||
1978 | 182 | def test_copy_sudoers(self, sph, tmpdir): | ||
1979 | 183 | """Check that sudoers file exists.""" | ||
1980 | 184 | sph.copy_sudoers() | ||
1981 | 185 | assert filecmp.cmp('./files/sudoers', tmpdir.join('/etc/sudoers')) |
Small comment on the pagerduty_proxy that may be removed in favor of the juju model-config if any.
Comments in line.
Other than that looks good to me.