Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
# One experiment per machine, so each one gets its own series on the dashboard.
# The file name must match the host exactly as written in the `hosts` inventory.
experiment_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
become_method: sudo

tasks:
- name: Check this host has its own dashboard experiment
assert:
that: experiment_id is defined
fail_msg: >-
No experiment_id for {{ inventory_hostname }}. Create the experiment on
the dashboard, then declare it in host_vars/{{ inventory_hostname }}.yml
rather than in vars/main.yml.
quiet: yes
- name: Create CodeCarbon group
group:
name: "{{ codecarbon_group }}"
Expand All @@ -31,17 +39,34 @@
apt:
name: python3-venv
state: present
# A venv hardcodes the Python minor version in lib/pythonX.Y/site-packages,
# but bin/python3 is only a symlink to /usr/bin/python3. After a distribution
# upgrade the interpreter no longer finds its own site-packages and even pip
# disappears. `creates:` would then skip the rebuild forever, so probe the
# venv instead of merely testing its presence.
- name: Check whether the existing virtual environment is still usable
command: "{{ codecarbon_venv }}/bin/python -c 'import pip'"
register: codecarbon_venv_check
changed_when: false
failed_when: false
- name: Remove the stale virtual environment
file:
path: "{{ codecarbon_venv }}"
state: absent
when: codecarbon_venv_check.rc != 0
# The venv is built and owned by root: the service account only needs to read
# it, and it has no home directory for Ansible to write its temporary files.
- name: Create Python virtual environment
command:
cmd: "python3 -m venv {{ codecarbon_venv }}"
creates: "{{ codecarbon_venv }}"
become_user: "{{ codecarbon_user }}"
creates: "{{ codecarbon_venv }}/bin/python"
- name: Install CodeCarbon package
pip:
name: codecarbon
state: latest
virtualenv: "{{ codecarbon_venv }}"
become_user: "{{ codecarbon_user }}"
- name: Create CodeCarbon configuration file
register: codecarbon_config
template:
src: ../templates/codecarbon.config.j2
dest: "{{ codecarbon_home }}/.codecarbon.config"
Expand Down
85 changes: 50 additions & 35 deletions deploy/ansible/codecarbon_cli_as_a_service/tasks/rapl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,55 @@
become_method: sudo

tasks:
- name: Set RAPL directory permissions
shell: |
chmod -R g+r {{ rapl_base_path }}/*
chown -R root:{{ codecarbon_group }} {{ rapl_base_path }}/*
ignore_errors: yes # In case RAPL is not available
# This does not work because there is loop in folder symlink
# - name: Set RAPL directory permissions
# file:
# path: "{{ rapl_base_path }}"
# state: directory
# owner: root
# group: "{{ codecarbon_group }}"
# mode: "g+r"
# recurse: yes
# ignore_errors: yes # In case RAPL is not available
- name: Install sysfsutils
apt:
name: sysfsutils
state: present
- name: Find all RAPL energy_uj files
# A udev rule is used instead of a one-off chmod so the permissions survive
# reboots, and instead of sysfsutils so it does not depend on a Debian-only
# package. The rule fires once per powercap device, so every RAPL domain is
# covered without enumerating them.
#
# Only energy_uj is exposed, only to the CodeCarbon group, and read-only:
# energy counters are a power side channel (CVE-2020-8694, PLATYPUS).
- name: Install the udev rule granting RAPL access to the CodeCarbon group
copy:
dest: /etc/udev/rules.d/99-codecarbon-rapl.rules
owner: root
group: root
mode: "0644"
content: |
# Managed by Ansible - CodeCarbon
SUBSYSTEM=="powercap", ACTION=="add", TEST=="energy_uj", \
RUN+="/bin/chgrp {{ codecarbon_group }} /sys%p/energy_uj", \
RUN+="/bin/chmod 0440 /sys%p/energy_uj"
register: rapl_udev_rule

- name: Apply the rule to the RAPL devices already present
command: "{{ item }}"
loop:
- udevadm control --reload-rules
- udevadm trigger --subsystem-match=powercap --action=add
when: rapl_udev_rule.changed

- name: List the RAPL energy counters
find:
paths: "{{ rapl_base_path }}"
patterns:
- "energy_uj"
- "name"
recurse: yes
register: rapl_files
- name: Configure sysfs for RAPL permissions
blockinfile:
path: /etc/sysfs.conf
create: yes
block: |
{% for file in rapl_files.files %}
mode {{ file.path | replace('/sys/','class/') }} = 0440
owner {{ file.path | replace('/sys/','class/') }} = root:{{ codecarbon_group }}
{% endfor %}
paths: "{{ rapl_base_path }}"
patterns: energy_uj
recurse: yes
follow: no
register: rapl_counters
ignore_errors: yes # In case RAPL is not available

# The check is done on the gid, not on gr_name: when another group shares the
# same gid, the name resolves to whichever entry comes first in /etc/group,
# which would report a failure even though access is actually granted.
- name: Look up the CodeCarbon group id
getent:
database: group
key: "{{ codecarbon_group }}"

- name: Report RAPL counters that are still unreadable by CodeCarbon
debug:
msg: >-
No RAPL counter readable by the {{ codecarbon_group }} group was found.
CodeCarbon will fall back to power estimation instead of measurement.
when: rapl_counters.files | default([])
| selectattr('gid', 'equalto', ansible_facts.getent_group[codecarbon_group][1] | int)
| list | length == 0
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@
name: codecarbon
enabled: yes
state: started
daemon_reload: yes
daemon_reload: yes

# CodeCarbon only reads .codecarbon.config at startup, so a configuration
# change is a no-op until the service is restarted. The variable comes from
# the previous play, registered variables are kept per host across plays.
- name: Restart CodeCarbon to pick up the new configuration
systemd:
name: codecarbon
state: restarted
when: codecarbon_config.changed | default(false)
5 changes: 4 additions & 1 deletion deploy/ansible/codecarbon_cli_as_a_service/vars/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ codecarbon_venv: "{{ codecarbon_home }}/venv"
api_endpoint: https://api.codecarbon.io
organization_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
project_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
experiment_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
api_key: xxx_xxxxxxxx_xxxxxxxxxxxx

# experiment_id is per host: see host_vars/<inventory_hostname>.yml.
# Do not define it here, vars_files takes precedence over host_vars and would
# silently send every machine to the same dashboard experiment.
49 changes: 46 additions & 3 deletions docs/how-to/ansible.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The Ansible playbook automates the following tasks:
``` text
codecarbon/deploy/ansible/codecarbon_cli_as_a_service/
β”œβ”€β”€ hosts
β”œβ”€β”€ host_vars
β”‚ └── yourservername.yourdomain.com.yml
β”œβ”€β”€ tasks
β”‚ β”œβ”€β”€ install_codecarbon.yml
β”‚ β”œβ”€β”€ main.yml
Expand All @@ -53,23 +55,64 @@ yourservername.yourdomain.com hostname=yourservername ansible_user=root ansibl

### Step 2: Update Ansible Variables

Update your CodeCarbon API credentials in `vars/main.yml`:
Update your CodeCarbon API credentials in `vars/main.yml`. These are shared by
every machine the playbook installs:

``` yaml
organization_id: your_org_id
project_id: your_project_id
experiment_id: your_experiment_id
api_key: your_api_key
```

### Step 3: Run the Playbook
### Step 3: Give Each Machine Its Own Experiment

An experiment identifies a single monitored machine on the dashboard. If several
servers report under the same `experiment_id`, their measurements are mixed into
one series and you can no longer tell them apart, so create one experiment per
machine on the dashboard.

Declare it in `host_vars/`, next to the `hosts` inventory. The file name must
match the host exactly as written in the inventory:

``` yaml
# host_vars/yourservername.yourdomain.com.yml
experiment_id: your_experiment_id_for_this_server
```

Add one such file per machine. Ansible loads it automatically, no change to the
playbook is needed:

``` text
host_vars
β”œβ”€β”€ firstserver.yourdomain.com.yml
└── secondserver.yourdomain.com.yml
```

Do not put `experiment_id` back into `vars/main.yml`. That file is loaded with
`vars_files`, which takes precedence over `host_vars`, so a value left there
overrides every per-host file and silently sends all your machines to the same
experiment. The playbook stops with an explicit error if a host has no
`experiment_id` at all.

You can check what each host resolves to before deploying:

``` bash
ansible all -i hosts -m debug -a 'msg="{{ inventory_hostname }} -> {{ experiment_id }}"'
```

### Step 4: Run the Playbook

Execute the Ansible playbook to deploy CodeCarbon:

``` bash
ansible-playbook -i hosts tasks/main.yml
```

The playbook is idempotent, so you can run it again to add a machine or to
update the configuration of the existing ones. Changing the configuration
restarts the service, because CodeCarbon only reads `.codecarbon.config` at
startup.

## Next Steps

- [Install CodeCarbon as a Linux Service](linux-service.md) for manual setup details
Expand Down
Loading
Loading