Skip to content

Commit 7210aea

Browse files
committed
Add resources for dealing with the system python
1 parent 49e504d commit 7210aea

File tree

18 files changed

+768
-46
lines changed

18 files changed

+768
-46
lines changed

cookbooks/boxcutter_python/README.md

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
# boxcutter_python
22

3-
Manage multiple side-by-side Python environments with pyenv.
3+
Configure Python, Python packages and virtual environments using the
4+
system Python and/or multiple side-by-side Python environments with
5+
pyenv.
46

5-
## Description
7+
## Configuring system Python
8+
9+
Some basic primitive resources are provided for working with the system python:
10+
- `boxcutter_python_virtualenv`
11+
- `boxcutter_python_package`
12+
13+
To use these resources, include the `boxcutter_python::system` recipe.
14+
15+
Here's an example that creates a virtualenv and installs a python package:
16+
17+
```ruby
18+
include_recipe 'boxcutter_python::system'
19+
20+
boxcutter_python_virtualenv '/opt/certbot/venv'
21+
22+
boxcutter_python_package 'certbot' do
23+
version '3.0'
24+
end
25+
```
26+
27+
## Configuring pyenv
628

729
This cookbook uses [pyenv](https://github.com/pyenv/pyenv) to install and
830
manage multiple versions of Python side-by-side on a single host. This allows
@@ -101,3 +123,72 @@ node.default['boxcutter_python']['python_build'] = {
101123
},
102124
}
103125
```
126+
127+
## Recipes
128+
129+
### `pyenv`
130+
131+
The `pyenv` recipe installs pyenv so that you can easily switch between multiple
132+
versions of Python.
133+
134+
### `system`
135+
136+
The `system` recipe installs the system Python - the default version of Python
137+
for a particular operating system.
138+
139+
## Resources
140+
141+
### `boxcutter_python_virtualenv`
142+
143+
The `boxcutter_python_virtualenv` resource creates a Python virtual environment.
144+
145+
```ruby
146+
boxcutter_python_virtualenv `/opt/certbot/venv`
147+
```
148+
149+
#### Actions
150+
151+
- `:create` - Create a Python virtual environment. *(default)*
152+
- `:delete` - Delete a Python virtual environment.
153+
154+
#### Properties
155+
156+
- `path` - The path to create the virtual environment.
157+
- `interpreter` - The Python interpreter used to run commands to configure the virtualenv.
158+
- `user` - The user name or user ID used to run commands in the Python interpreter.
159+
- `group` - The group name or group ID used to run commands in the Python interpreter.
160+
- `system_site_packages` - Install globally available packages to the system site-packages directory.
161+
- `copies` - Use copies rather than symlinks.
162+
- `clear` - Delete the contents of the virtual environment directory if it already exists, before creating.
163+
- `upgrade_deps` - Upgrade pip + setuptools to the latest on PyPI.
164+
- `without_pip` - Do not install pip in the virtualenv.
165+
- `prompt` - Set the prompt inside the virtualenv.
166+
167+
### `boxcutter_python_pip`
168+
169+
The `boxcutter_python_pip` resource installs Python packages using `pip`.
170+
171+
```ruby
172+
boxcutter_python_pip `certbot` do
173+
version '3.0'
174+
end
175+
```
176+
177+
#### Actions
178+
179+
- `:install` - Install a Python package. *(default)*
180+
- `:upgrade` - Install a Python package using the `--upgrade` flag.
181+
- `:remove` - Remove a Python package.
182+
183+
#### Properties
184+
185+
- `package_name` - 'The name of the Python package to install.'
186+
- `version` - 'The version of the Python package to install/upgrade.'
187+
- `pip_binary` - 'Path to the pip binary. Mutually exclusive with `virtualenv`.'
188+
- `virtualenv` - 'Path to a virtual environment in which to install the Python package.'
189+
- `user` - 'The user name or user ID used to run pip commands.'
190+
- `group` - 'The group name or group ID used to pip commands.'
191+
- `extra_options` - 'Extra options to pass to the pip command.'
192+
- `timeout` - 'The number of seconds to wait for the pip command to complete.'
193+
- `environment` - 'Hash containing environment varibles to set before the pip command is run.'
194+

cookbooks/boxcutter_python/kitchen.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ provisioner:
2424
transport:
2525
name: dokken
2626

27+
lifecycle:
28+
post_create:
29+
- remote: |
30+
bash -c -x '
31+
# Force firstboot
32+
touch /root/firstboot_os
33+
'
34+
2735
verifier:
2836
name: inspec
2937

@@ -40,6 +48,11 @@ platforms:
4048
image: boxcutter/dokken-ubuntu-22.04
4149
pid_one_command: /bin/systemd
4250

51+
- name: ubuntu-24.04
52+
driver:
53+
image: boxcutter/dokken-ubuntu-24.04
54+
pid_one_command: /bin/systemd
55+
4356
- name: centos-stream-9
4457
driver:
4558
image: boxcutter/dokken-centos-stream-9
@@ -59,3 +72,19 @@ suites:
5972
inspec_tests:
6073
- test/integration/default
6174
attributes:
75+
76+
- name: pyenv
77+
provisioner:
78+
policyfile_path: policyfiles/Policyfile.pyenv.rb
79+
verifier:
80+
inspec_tests:
81+
- test/integration/default
82+
attributes:
83+
84+
- name: system
85+
provisioner:
86+
policyfile_path: policyfiles/Policyfile.system.rb
87+
verifier:
88+
inspec_tests:
89+
- test/integration/system
90+
attributes:
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
require 'chef/mixin/shell_out'
2+
3+
module Boxcutter
4+
class Python
5+
module Helpers
6+
extend Chef::Mixin::ShellOut
7+
8+
def self.read_pyvenv_cfg(pyvenv_cfg_path)
9+
config = {}
10+
::File.foreach(pyvenv_cfg_path) do |line|
11+
# Skip comments or blank lines
12+
next if line.strip.empty? || line.strip.start_with?('#')
13+
14+
# Split each line into key-value pairs
15+
key, value = line.strip.split('=', 2)
16+
config[key.strip] = value.strip if key && value
17+
end
18+
config
19+
end
20+
21+
def self.remove_surrounding_single_quotes(string)
22+
if string.start_with?("'") && string.end_with?("'")
23+
string[1..-2]
24+
else
25+
string
26+
end
27+
end
28+
29+
# these methods are the required overrides of
30+
# a provider that extends from Chef::Provider::Package
31+
# so refactoring into core Chef should be easy
32+
33+
def self.current_installed_version(new_resource)
34+
@current_installed_version ||= begin
35+
# Normalize package name (e.g., replace underscores with hyphens)
36+
normalized_package_name = new_resource.package_name.gsub('_', '-')
37+
38+
# Command to get package details using pip3 show
39+
version_check_cmd = "#{which_pip(new_resource)} show #{normalized_package_name}"
40+
41+
# Run the command and capture the result
42+
result = shell_out(version_check_cmd)
43+
if result.exitstatus == 0
44+
# Extract the version from the 'Version:' line in `pip3 show` output
45+
result.stdout.match(/^Version:\s*(.+)$/i)[1]
46+
end
47+
end
48+
end
49+
50+
def self.candidate_version(new_resource)
51+
@candidate_version ||= new_resource.version||'latest'
52+
end
53+
54+
def self.install_package(version, new_resource)
55+
# if a version isn't specified (latest), is a source archive
56+
# (ex. http://my.package.repo/SomePackage-1.0.4.zip),
57+
# or from a VCS (ex. git+https://git.repo/some_pkg.git) then do not
58+
# append a version as this will break the source link
59+
if version == 'latest' || \
60+
new_resource.package_name.downcase.start_with?('http:', 'https:') || \
61+
['git', 'hg', 'svn'].include?(new_resource.package_name.downcase.split('+')[0])
62+
version = ''
63+
else
64+
version = "==#{version}"
65+
end
66+
pip_cmd('install', version, new_resource)
67+
end
68+
69+
def self.upgrade_package(version, new_resource)
70+
# Upgrades are just an install with the `--upgrade` parameter added
71+
new_resource.options "#{new_resource.options} --upgrade"
72+
install_package(version, new_resource)
73+
end
74+
75+
def self.remove_package(_version, new_resource)
76+
new_resource.options "#{new_resource.options} --yes"
77+
# Python only allows one version to be installed at a time, so it's
78+
# not necessary to provide a version on uninstall.
79+
pip_cmd('uninstall', '', new_resource)
80+
end
81+
82+
def self.removing_package?(current_resource, new_resource)
83+
if current_resource.version.nil?
84+
false # nothing to remove
85+
elsif new_resource.version.nil?
86+
true # remove any version of a package
87+
else
88+
new_resource.version == current_resource.version # we don't have the version we want to remove
89+
end
90+
end
91+
92+
def self.pip_cmd(subcommand, version = '', new_resource)
93+
options = { :timeout => new_resource.timeout, :user => new_resource.user, :group => new_resource.group }
94+
environment = {}
95+
environment['HOME'] = Dir.home(new_resource.user) if new_resource.user
96+
environment.merge!(new_resource.environment) if new_resource.environment && !new_resource.environment.empty?
97+
options[:environment] = environment
98+
shell_out!(
99+
"#{which_pip(new_resource)} #{subcommand} #{new_resource.extra_options}" \
100+
"#{new_resource.package_name}#{version}", **options
101+
)
102+
end
103+
104+
def self.which_pip(new_resource)
105+
if new_resource.respond_to?('virtualenv') && new_resource.virtualenv
106+
::File.join(new_resource.virtualenv, '/bin/pip')
107+
else
108+
new_resource.pip_binary
109+
end
110+
end
111+
end
112+
end
113+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Policyfile.pyenv.rb - Describe how you want Chef Infra Client to build your system.
2+
#
3+
# For more information on the Policyfile feature, visit
4+
# https://docs.chef.io/policyfile/
5+
6+
# A name that describes what the system you're building with Chef does.
7+
name 'boxcutter_python'
8+
9+
# Where to find external cookbooks:
10+
default_source :chef_repo, '../../../../chef-cookbooks/cookbooks'
11+
default_source :chef_repo, '../../'
12+
13+
# run_list: chef-client will run these recipes in the order specified.
14+
run_list 'boxcutter_ohai', 'boxcutter_init', 'boxcutter_python_test::pyenv'
15+
16+
# Specify a custom source for a single cookbook:
17+
cookbook 'boxcutter_python', path: '..'
18+
cookbook 'boxcutter_python_test', path: '../test/cookbooks/boxcutter_python_test'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Policyfile.system.rb - Describe how you want Chef Infra Client to build your system.
2+
#
3+
# For more information on the Policyfile feature, visit
4+
# https://docs.chef.io/policyfile/
5+
6+
# A name that describes what the system you're building with Chef does.
7+
name 'boxcutter_python'
8+
9+
# Where to find external cookbooks:
10+
default_source :chef_repo, '../../../../chef-cookbooks/cookbooks'
11+
default_source :chef_repo, '../../'
12+
13+
# run_list: chef-client will run these recipes in the order specified.
14+
run_list 'boxcutter_ohai', 'boxcutter_init', 'boxcutter_python_test::system'
15+
16+
# Specify a custom source for a single cookbook:
17+
cookbook 'boxcutter_python', path: '..'
18+
cookbook 'boxcutter_python_test', path: '../test/cookbooks/boxcutter_python_test'

cookbooks/boxcutter_python/recipes/default.rb

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,3 @@
1515
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1616
# See the License for the specific language governing permissions and
1717
# limitations under the License.
18-
19-
case node['platform_family']
20-
when 'rhel'
21-
package %w{
22-
git
23-
gcc
24-
zlib-devel
25-
bzip2
26-
bzip2-devel
27-
readline-devel
28-
sqlite
29-
sqlite-devel
30-
openssl-devel
31-
tk-devel
32-
libffi-devel
33-
xz-devel
34-
} do
35-
action :upgrade
36-
end
37-
when 'debian'
38-
package %w{
39-
build-essential
40-
git
41-
libssl-dev
42-
zlib1g-dev
43-
libbz2-dev
44-
libreadline-dev
45-
libsqlite3-dev
46-
wget
47-
curl
48-
llvm
49-
libncursesw5-dev
50-
xz-utils
51-
tk-dev
52-
libxml2-dev
53-
libxmlsec1-dev
54-
libffi-dev
55-
liblzma-dev
56-
} do
57-
action :upgrade
58-
end
59-
end
60-
61-
boxcutter_python_pyenv 'manage'

0 commit comments

Comments
 (0)