Skip to content

Commit 1a8f172

Browse files
cooktheryanrussozfelixfontein
authored
Introduce bootc functionality (#8606)
* introduce bootc functionality Signed-off-by: Ryan Cook <[email protected]> Co-authored-by: Alexei Znamensky <[email protected]> * fix of test Signed-off-by: Ryan Cook <[email protected]> * switch stdout var Signed-off-by: Ryan Cook <[email protected]> * Feedback on NOTE format Co-authored-by: Felix Fontein <[email protected]> * addition of trailing comma Co-authored-by: Felix Fontein <[email protected]> * addition of trailing comma Co-authored-by: Felix Fontein <[email protected]> * incorporating feedback from russoz Signed-off-by: Ryan Cook <[email protected]> * error in stdout Signed-off-by: Ryan Cook <[email protected]> * proper rc checking and status Signed-off-by: Ryan Cook <[email protected]> * linting Signed-off-by: Ryan Cook <[email protected]> * Update version Co-authored-by: Felix Fontein <[email protected]> --------- Signed-off-by: Ryan Cook <[email protected]> Co-authored-by: Alexei Znamensky <[email protected]> Co-authored-by: Felix Fontein <[email protected]>
1 parent 52126b8 commit 1a8f172

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

.github/BOTMETA.yml

+2
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ files:
448448
maintainers: hkariti
449449
$modules/bitbucket_:
450450
maintainers: catcombo
451+
$modules/bootc_manage.py:
452+
maintainers: cooktheryan
451453
$modules/bower.py:
452454
maintainers: mwarkentin
453455
$modules/btrfs_:

plugins/modules/bootc_manage.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/python
2+
3+
# Copyright (c) 2024, Ryan Cook <[email protected]>
4+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt
5+
# or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
# SPDX-License-Identifier: GPL-3.0-or-later
7+
8+
from __future__ import (absolute_import, division, print_function)
9+
__metaclass__ = type
10+
11+
DOCUMENTATION = '''
12+
---
13+
module: bootc_manage
14+
version_added: 9.3.0
15+
author:
16+
- Ryan Cook (@cooktheryan)
17+
short_description: Bootc Switch and Upgrade
18+
description:
19+
- This module manages the switching and upgrading of C(bootc).
20+
options:
21+
state:
22+
description:
23+
- 'Control to apply the latest image or switch the image.'
24+
- 'B(Note:) This will not reboot the system.'
25+
- 'Please use M(ansible.builtin.reboot) to reboot the system.'
26+
required: true
27+
type: str
28+
choices: ['switch', 'latest']
29+
image:
30+
description:
31+
- 'The image to switch to.'
32+
- 'This is required when O(state=switch).'
33+
required: false
34+
type: str
35+
36+
'''
37+
38+
EXAMPLES = '''
39+
# Switch to a different image
40+
- name: Provide image to switch to a different image and retain the current running image
41+
community.general.bootc_manage:
42+
state: switch
43+
image: "example.com/image:latest"
44+
45+
# Apply updates of the current running image
46+
- name: Apply updates of the current running image
47+
community.general.bootc_manage:
48+
state: latest
49+
'''
50+
51+
RETURN = '''
52+
'''
53+
54+
55+
from ansible.module_utils.basic import AnsibleModule
56+
from ansible.module_utils.common.locale import get_best_parsable_locale
57+
58+
59+
def main():
60+
argument_spec = dict(
61+
state=dict(type='str', required=True, choices=['switch', 'latest']),
62+
image=dict(type='str', required=False),
63+
)
64+
module = AnsibleModule(
65+
argument_spec=argument_spec,
66+
required_if=[
67+
('state', 'switch', ['image']),
68+
],
69+
)
70+
71+
state = module.params['state']
72+
image = module.params['image']
73+
74+
if state == 'switch':
75+
command = ['bootc', 'switch', image, '--retain']
76+
elif state == 'latest':
77+
command = ['bootc', 'upgrade']
78+
79+
locale = get_best_parsable_locale(module)
80+
module.run_command_environ_update = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LC_CTYPE=locale, LANGUAGE=locale)
81+
rc, stdout, err = module.run_command(command, check_rc=True)
82+
83+
if 'Queued for next boot: ' in stdout:
84+
result = {'changed': True, 'stdout': stdout}
85+
module.exit_json(**result)
86+
elif 'No changes in ' in stdout or 'Image specification is unchanged.' in stdout:
87+
result = {'changed': False, 'stdout': stdout}
88+
module.exit_json(**result)
89+
else:
90+
result = {'changed': False, 'stderr': err}
91+
module.fail_json(msg='ERROR: Command execution failed.', **result)
92+
93+
94+
if __name__ == '__main__':
95+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright (c) Ansible project
2+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from __future__ import (absolute_import, division, print_function)
6+
__metaclass__ = type
7+
8+
from ansible_collections.community.general.tests.unit.compat.mock import patch
9+
from ansible_collections.community.general.plugins.modules import bootc_manage
10+
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
11+
12+
13+
class TestBootcManageModule(ModuleTestCase):
14+
15+
def setUp(self):
16+
super(TestBootcManageModule, self).setUp()
17+
self.module = bootc_manage
18+
19+
def tearDown(self):
20+
super(TestBootcManageModule, self).tearDown()
21+
22+
def test_switch_without_image(self):
23+
"""Failure if state is 'switch' but no image provided"""
24+
set_module_args({'state': 'switch'})
25+
with self.assertRaises(AnsibleFailJson) as result:
26+
self.module.main()
27+
self.assertEqual(result.exception.args[0]['msg'], "state is switch but all of the following are missing: image")
28+
29+
def test_switch_with_image(self):
30+
"""Test successful switch with image provided"""
31+
set_module_args({'state': 'switch', 'image': 'example.com/image:latest'})
32+
with patch('ansible.module_utils.basic.AnsibleModule.run_command') as run_command_mock:
33+
run_command_mock.return_value = (0, 'Queued for next boot: ', '')
34+
with self.assertRaises(AnsibleExitJson) as result:
35+
self.module.main()
36+
self.assertTrue(result.exception.args[0]['changed'])
37+
38+
def test_latest_state(self):
39+
"""Test successful upgrade to the latest state"""
40+
set_module_args({'state': 'latest'})
41+
with patch('ansible.module_utils.basic.AnsibleModule.run_command') as run_command_mock:
42+
run_command_mock.return_value = (0, 'Queued for next boot: ', '')
43+
with self.assertRaises(AnsibleExitJson) as result:
44+
self.module.main()
45+
self.assertTrue(result.exception.args[0]['changed'])
46+
47+
def test_latest_state_no_change(self):
48+
"""Test no change for latest state"""
49+
set_module_args({'state': 'latest'})
50+
with patch('ansible.module_utils.basic.AnsibleModule.run_command') as run_command_mock:
51+
run_command_mock.return_value = (0, 'No changes in ', '')
52+
with self.assertRaises(AnsibleExitJson) as result:
53+
self.module.main()
54+
self.assertFalse(result.exception.args[0]['changed'])
55+
56+
def test_switch_image_failure(self):
57+
"""Test failure during image switch"""
58+
set_module_args({'state': 'switch', 'image': 'example.com/image:latest'})
59+
with patch('ansible.module_utils.basic.AnsibleModule.run_command') as run_command_mock:
60+
run_command_mock.return_value = (1, '', 'ERROR')
61+
with self.assertRaises(AnsibleFailJson) as result:
62+
self.module.main()
63+
self.assertEqual(result.exception.args[0]['msg'], 'ERROR: Command execution failed.')
64+
65+
def test_latest_state_failure(self):
66+
"""Test failure during upgrade"""
67+
set_module_args({'state': 'latest'})
68+
with patch('ansible.module_utils.basic.AnsibleModule.run_command') as run_command_mock:
69+
run_command_mock.return_value = (1, '', 'ERROR')
70+
with self.assertRaises(AnsibleFailJson) as result:
71+
self.module.main()
72+
self.assertEqual(result.exception.args[0]['msg'], 'ERROR: Command execution failed.')

0 commit comments

Comments
 (0)