Skip to content

Commit

Permalink
Add resources for dealing with the system python
Browse files Browse the repository at this point in the history
  • Loading branch information
taylorific committed Jan 8, 2025
1 parent 49e504d commit 7210aea
Show file tree
Hide file tree
Showing 18 changed files with 768 additions and 46 deletions.
95 changes: 93 additions & 2 deletions cookbooks/boxcutter_python/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
# boxcutter_python

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

## Description
## Configuring system Python

Some basic primitive resources are provided for working with the system python:
- `boxcutter_python_virtualenv`
- `boxcutter_python_package`

To use these resources, include the `boxcutter_python::system` recipe.

Here's an example that creates a virtualenv and installs a python package:

```ruby
include_recipe 'boxcutter_python::system'

boxcutter_python_virtualenv '/opt/certbot/venv'

boxcutter_python_package 'certbot' do
version '3.0'
end
```

## Configuring pyenv

This cookbook uses [pyenv](https://github.com/pyenv/pyenv) to install and
manage multiple versions of Python side-by-side on a single host. This allows
Expand Down Expand Up @@ -101,3 +123,72 @@ node.default['boxcutter_python']['python_build'] = {
},
}
```

## Recipes

### `pyenv`

The `pyenv` recipe installs pyenv so that you can easily switch between multiple
versions of Python.

### `system`

The `system` recipe installs the system Python - the default version of Python
for a particular operating system.

## Resources

### `boxcutter_python_virtualenv`

The `boxcutter_python_virtualenv` resource creates a Python virtual environment.

```ruby
boxcutter_python_virtualenv `/opt/certbot/venv`
```

#### Actions

- `:create` - Create a Python virtual environment. *(default)*
- `:delete` - Delete a Python virtual environment.

#### Properties

- `path` - The path to create the virtual environment.
- `interpreter` - The Python interpreter used to run commands to configure the virtualenv.
- `user` - The user name or user ID used to run commands in the Python interpreter.
- `group` - The group name or group ID used to run commands in the Python interpreter.
- `system_site_packages` - Install globally available packages to the system site-packages directory.
- `copies` - Use copies rather than symlinks.
- `clear` - Delete the contents of the virtual environment directory if it already exists, before creating.
- `upgrade_deps` - Upgrade pip + setuptools to the latest on PyPI.
- `without_pip` - Do not install pip in the virtualenv.
- `prompt` - Set the prompt inside the virtualenv.

### `boxcutter_python_pip`

The `boxcutter_python_pip` resource installs Python packages using `pip`.

```ruby
boxcutter_python_pip `certbot` do
version '3.0'
end
```

#### Actions

- `:install` - Install a Python package. *(default)*
- `:upgrade` - Install a Python package using the `--upgrade` flag.
- `:remove` - Remove a Python package.

#### Properties

- `package_name` - 'The name of the Python package to install.'
- `version` - 'The version of the Python package to install/upgrade.'
- `pip_binary` - 'Path to the pip binary. Mutually exclusive with `virtualenv`.'
- `virtualenv` - 'Path to a virtual environment in which to install the Python package.'
- `user` - 'The user name or user ID used to run pip commands.'
- `group` - 'The group name or group ID used to pip commands.'
- `extra_options` - 'Extra options to pass to the pip command.'
- `timeout` - 'The number of seconds to wait for the pip command to complete.'
- `environment` - 'Hash containing environment varibles to set before the pip command is run.'

29 changes: 29 additions & 0 deletions cookbooks/boxcutter_python/kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ provisioner:
transport:
name: dokken

lifecycle:
post_create:
- remote: |
bash -c -x '
# Force firstboot
touch /root/firstboot_os
'
verifier:
name: inspec

Expand All @@ -40,6 +48,11 @@ platforms:
image: boxcutter/dokken-ubuntu-22.04
pid_one_command: /bin/systemd

- name: ubuntu-24.04
driver:
image: boxcutter/dokken-ubuntu-24.04
pid_one_command: /bin/systemd

- name: centos-stream-9
driver:
image: boxcutter/dokken-centos-stream-9
Expand All @@ -59,3 +72,19 @@ suites:
inspec_tests:
- test/integration/default
attributes:

- name: pyenv
provisioner:
policyfile_path: policyfiles/Policyfile.pyenv.rb
verifier:
inspec_tests:
- test/integration/default
attributes:

- name: system
provisioner:
policyfile_path: policyfiles/Policyfile.system.rb
verifier:
inspec_tests:
- test/integration/system
attributes:
113 changes: 113 additions & 0 deletions cookbooks/boxcutter_python/libraries/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require 'chef/mixin/shell_out'

module Boxcutter
class Python
module Helpers
extend Chef::Mixin::ShellOut

def self.read_pyvenv_cfg(pyvenv_cfg_path)
config = {}
::File.foreach(pyvenv_cfg_path) do |line|
# Skip comments or blank lines
next if line.strip.empty? || line.strip.start_with?('#')

# Split each line into key-value pairs
key, value = line.strip.split('=', 2)
config[key.strip] = value.strip if key && value
end
config
end

def self.remove_surrounding_single_quotes(string)
if string.start_with?("'") && string.end_with?("'")
string[1..-2]
else
string
end
end

# these methods are the required overrides of
# a provider that extends from Chef::Provider::Package
# so refactoring into core Chef should be easy

def self.current_installed_version(new_resource)
@current_installed_version ||= begin
# Normalize package name (e.g., replace underscores with hyphens)
normalized_package_name = new_resource.package_name.gsub('_', '-')

# Command to get package details using pip3 show
version_check_cmd = "#{which_pip(new_resource)} show #{normalized_package_name}"

# Run the command and capture the result
result = shell_out(version_check_cmd)
if result.exitstatus == 0
# Extract the version from the 'Version:' line in `pip3 show` output
result.stdout.match(/^Version:\s*(.+)$/i)[1]
end
end
end

def self.candidate_version(new_resource)
@candidate_version ||= new_resource.version||'latest'
end

def self.install_package(version, new_resource)
# if a version isn't specified (latest), is a source archive
# (ex. http://my.package.repo/SomePackage-1.0.4.zip),
# or from a VCS (ex. git+https://git.repo/some_pkg.git) then do not
# append a version as this will break the source link
if version == 'latest' || \
new_resource.package_name.downcase.start_with?('http:', 'https:') || \
['git', 'hg', 'svn'].include?(new_resource.package_name.downcase.split('+')[0])
version = ''
else
version = "==#{version}"
end
pip_cmd('install', version, new_resource)
end

def self.upgrade_package(version, new_resource)
# Upgrades are just an install with the `--upgrade` parameter added
new_resource.options "#{new_resource.options} --upgrade"
install_package(version, new_resource)
end

def self.remove_package(_version, new_resource)
new_resource.options "#{new_resource.options} --yes"
# Python only allows one version to be installed at a time, so it's
# not necessary to provide a version on uninstall.
pip_cmd('uninstall', '', new_resource)
end

def self.removing_package?(current_resource, new_resource)
if current_resource.version.nil?
false # nothing to remove
elsif new_resource.version.nil?
true # remove any version of a package
else
new_resource.version == current_resource.version # we don't have the version we want to remove
end
end

def self.pip_cmd(subcommand, version = '', new_resource)
options = { :timeout => new_resource.timeout, :user => new_resource.user, :group => new_resource.group }
environment = {}
environment['HOME'] = Dir.home(new_resource.user) if new_resource.user
environment.merge!(new_resource.environment) if new_resource.environment && !new_resource.environment.empty?
options[:environment] = environment
shell_out!(
"#{which_pip(new_resource)} #{subcommand} #{new_resource.extra_options}" \
"#{new_resource.package_name}#{version}", **options
)
end

def self.which_pip(new_resource)
if new_resource.respond_to?('virtualenv') && new_resource.virtualenv
::File.join(new_resource.virtualenv, '/bin/pip')
else
new_resource.pip_binary
end
end
end
end
end
18 changes: 18 additions & 0 deletions cookbooks/boxcutter_python/policyfiles/Policyfile.pyenv.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Policyfile.pyenv.rb - Describe how you want Chef Infra Client to build your system.
#
# For more information on the Policyfile feature, visit
# https://docs.chef.io/policyfile/

# A name that describes what the system you're building with Chef does.
name 'boxcutter_python'

# Where to find external cookbooks:
default_source :chef_repo, '../../../../chef-cookbooks/cookbooks'
default_source :chef_repo, '../../'

# run_list: chef-client will run these recipes in the order specified.
run_list 'boxcutter_ohai', 'boxcutter_init', 'boxcutter_python_test::pyenv'

# Specify a custom source for a single cookbook:
cookbook 'boxcutter_python', path: '..'
cookbook 'boxcutter_python_test', path: '../test/cookbooks/boxcutter_python_test'
18 changes: 18 additions & 0 deletions cookbooks/boxcutter_python/policyfiles/Policyfile.system.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Policyfile.system.rb - Describe how you want Chef Infra Client to build your system.
#
# For more information on the Policyfile feature, visit
# https://docs.chef.io/policyfile/

# A name that describes what the system you're building with Chef does.
name 'boxcutter_python'

# Where to find external cookbooks:
default_source :chef_repo, '../../../../chef-cookbooks/cookbooks'
default_source :chef_repo, '../../'

# run_list: chef-client will run these recipes in the order specified.
run_list 'boxcutter_ohai', 'boxcutter_init', 'boxcutter_python_test::system'

# Specify a custom source for a single cookbook:
cookbook 'boxcutter_python', path: '..'
cookbook 'boxcutter_python_test', path: '../test/cookbooks/boxcutter_python_test'
44 changes: 0 additions & 44 deletions cookbooks/boxcutter_python/recipes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

case node['platform_family']
when 'rhel'
package %w{
git
gcc
zlib-devel
bzip2
bzip2-devel
readline-devel
sqlite
sqlite-devel
openssl-devel
tk-devel
libffi-devel
xz-devel
} do
action :upgrade
end
when 'debian'
package %w{
build-essential
git
libssl-dev
zlib1g-dev
libbz2-dev
libreadline-dev
libsqlite3-dev
wget
curl
llvm
libncursesw5-dev
xz-utils
tk-dev
libxml2-dev
libxmlsec1-dev
libffi-dev
liblzma-dev
} do
action :upgrade
end
end

boxcutter_python_pyenv 'manage'
Loading

0 comments on commit 7210aea

Please sign in to comment.