diff --git a/README.md b/README.md index 1c5d2665..95ad4afa 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ pipx inject ansible-core requests pynetbox pyutils pytz netaddr ```bash pipx install ansible-lint + ``` ```bash diff --git a/depin/core/roles/metrics/defaults/main.yml b/depin/core/roles/metrics/defaults/main.yml index 48769996..ec4c5b74 100644 --- a/depin/core/roles/metrics/defaults/main.yml +++ b/depin/core/roles/metrics/defaults/main.yml @@ -8,3 +8,4 @@ metrics_port: autonomi: 33002 hychain: 33003 storagechain: 33004 + avalanche: 33006 diff --git a/depin/services/roles/avalanche/defaults/main.yml b/depin/services/roles/avalanche/defaults/main.yml new file mode 100644 index 00000000..b3a6159f --- /dev/null +++ b/depin/services/roles/avalanche/defaults/main.yml @@ -0,0 +1,36 @@ +# defaults/main.yml + +# Command to execute (install, uninstall, etc.) +avalanche_cmd: "{{ depin_cmd | default('install') }}" + +# AvalancheGo version to install +avalanche_version: "1.11.11" + +# Determine the architecture (amd64 or arm64) +avalanche_arch: "{{ 'arm64' if ansible_facts['architecture'] == 'aarch64' else 'amd64' }}" + +# Download URL for AvalancheGo binary +avalanche_download_url: "https://github.com/ava-labs/avalanchego/releases/download/v{{ avalanche_version }}/avalanchego-linux-{{ avalanche_arch }}-v{{ avalanche_version }}.tar.gz" + +# Installation directory for AvalancheGo +avalanche_install_dir: '/opt/avalanche/{{ avalanche_version }}' + +# Configuration directory for AvalancheGo +avalanche_config_dir: '/home/{{ ansible_user_id }}/.avalanchego' # Or another appropriate path + +# User and group to run AvalancheGo service +avalanche_user: '{{ ansible_user_id }}' +avalanche_group: '{{ ansible_user_id }}' + +# Network ID ('mainnet' or 'fuji' for testnet) +avalanche_network_id: 'mainnet' + +# Additional necessary variables +avalanche_ip_mode: 'dynamic' # 'dynamic' or 'static' +avalanche_public_ip: '' # Automatically determined if empty and ip_mode == 'static' +avalanche_rpc_public: false # true or false +avalanche_admin_api_enabled: false # true or false +avalanche_index_enabled: false # true or false +avalanche_state_sync_enabled: true # true or false +avalanche_archival_mode: false # true or false +avalanche_db_dir: '' # Path to database directory if needed diff --git a/depin/services/roles/avalanche/tasks/commands/install.yml b/depin/services/roles/avalanche/tasks/commands/install.yml new file mode 100644 index 00000000..236cd30d --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/install.yml @@ -0,0 +1,209 @@ +# tasks/commands/install.yml + +--- + +- name: Determine OS family + set_fact: + os_family: "{{ ansible_facts['os_family'] }}" + +- name: Install required packages for AvalancheGo (Debian) + become: true + ansible.builtin.package: + name: + - curl + - wget + - dnsutils + state: present + when: os_family == 'Debian' + +- name: Install required packages for AvalancheGo (RedHat) + become: true + ansible.builtin.package: + name: + - curl + - wget + - bind-utils + - policycoreutils-python-utils + - policycoreutils + state: present + when: os_family == 'RedHat' + +- name: Set avalanche architecture + set_fact: + avalanche_arch: "{{ 'arm64' if ansible_facts['architecture'] == 'aarch64' else 'amd64' }}" + +- name: Set download URL for AvalancheGo + set_fact: + avalanche_download_url: "https://github.com/ava-labs/avalanchego/releases/download/v{{ avalanche_version }}/avalanchego-linux-{{ avalanche_arch }}-v{{ avalanche_version }}.tar.gz" + +- name: Get public IP address + ansible.builtin.uri: + url: https://api.ipify.org + return_content: yes + register: public_ip + when: + - avalanche_public_ip == '' + - avalanche_ip_mode == 'static' + +- name: Set public IP address fact + set_fact: + avalanche_public_ip: "{{ public_ip.content | trim }}" + when: + - avalanche_public_ip == '' + - avalanche_ip_mode == 'static' + +- name: Create program folder + become: true + ansible.builtin.file: + state: directory + owner: root + group: root + mode: '0755' + dest: "{{ avalanche_install_dir }}" + +- name: Verify that the program folder was created + ansible.builtin.stat: + path: "{{ avalanche_install_dir }}" + register: avalanche_program_folder + +- name: Assert that the program folder exists + ansible.builtin.assert: + that: + - avalanche_program_folder.stat.exists + - avalanche_program_folder.stat.isdir + +- name: Download AvalancheGo + become: true + ansible.builtin.get_url: + url: "{{ avalanche_download_url }}" + dest: /tmp/avalanchego.tar.gz + mode: '0644' + register: download_result + retries: 3 + delay: 10 + until: download_result is succeeded + +- name: Fail if AvalancheGo download failed + ansible.builtin.fail: + msg: "Failed to download AvalancheGo from {{ avalanche_download_url }}" + when: download_result is failed + +- name: Extract AvalancheGo + become: true + ansible.builtin.unarchive: + src: /tmp/avalanchego.tar.gz + dest: "{{ avalanche_install_dir }}" + remote_src: yes + extra_opts: + - --strip-components=1 + +- name: Ensure avalanchego binary is executable + become: true + ansible.builtin.file: + path: "{{ avalanche_install_dir }}/avalanchego" + mode: '0755' + +- name: Verify that the AvalancheGo binary exists + ansible.builtin.stat: + path: "{{ avalanche_install_dir }}/avalanchego" + register: avalanchego_binary + +- name: Assert that the AvalancheGo binary exists + ansible.builtin.assert: + that: + - avalanchego_binary.stat.exists + - (avalanchego_binary.stat.mode | int == 755) or (avalanchego_binary.stat.mode | int == 493) + +- name: Adjust SELinux context for avalanchego binary + become: true + ansible.builtin.command: "semanage fcontext -a -t bin_t '{{ avalanche_install_dir }}/avalanchego' || semanage fcontext -m -t bin_t '{{ avalanche_install_dir }}/avalanchego'" + args: + warn: false + when: ansible_facts['os_family'] == 'RedHat' + +- name: Restore SELinux context + become: true + ansible.builtin.command: "restorecon -Fv '{{ avalanche_install_dir }}/avalanchego'" + when: ansible_facts['os_family'] == 'RedHat' + +- name: Symlink avalanchego binary + become: true + ansible.builtin.file: + src: "{{ avalanche_install_dir }}/avalanchego" + dest: /usr/local/bin/avalanchego + state: link + force: true + +- name: Verify symlink for AvalancheGo binary + ansible.builtin.stat: + path: /usr/local/bin/avalanchego + follow: false + register: avalanchego_symlink + +- name: Assert that the symlink exists and is correct + ansible.builtin.assert: + that: + - avalanchego_symlink.stat.islnk + - avalanchego_symlink.stat.lnk_source == "{{ avalanche_install_dir }}/avalanchego" + +- name: Create configuration directories + become: true + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ avalanche_user }}" + group: "{{ avalanche_group }}" + mode: '0755' + loop: + - "{{ avalanche_config_dir }}/configs" + - "{{ avalanche_config_dir }}/configs/chains/C" + +- name: Deploy node.json configuration + become: true + ansible.builtin.template: + src: node.json.j2 + dest: "{{ avalanche_config_dir }}/configs/node.json" + mode: '0644' + owner: "{{ avalanche_user }}" + group: "{{ avalanche_group }}" + +- name: Deploy config.json configuration + become: true + ansible.builtin.template: + src: config.json.j2 + dest: "{{ avalanche_config_dir }}/configs/chains/C/config.json" + mode: '0644' + owner: "{{ avalanche_user }}" + group: "{{ avalanche_group }}" + +- name: Create systemd service + become: true + ansible.builtin.template: + src: templates/systemd.service.j2 + dest: /etc/systemd/system/avalanchego.service + mode: '0644' + +- name: Verify that the AvalancheGo service file exists + ansible.builtin.stat: + path: /etc/systemd/system/avalanchego.service + register: avalanchego_service_file + +- name: Assert that the AvalancheGo service file exists + ansible.builtin.assert: + that: + - avalanchego_service_file.stat.exists + +- name: Reload systemd daemon + become: true + ansible.builtin.systemd: + daemon_reload: yes + +- name: Start Avalanche node + ansible.builtin.include_tasks: + file: commands/start.yml + +- name: Install metrics + vars: + depin_cmd: install + ansible.builtin.include_tasks: + file: tasks/metrics.yml diff --git a/depin/services/roles/avalanche/tasks/commands/reset.yml b/depin/services/roles/avalanche/tasks/commands/reset.yml new file mode 100644 index 00000000..21de3f84 --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/reset.yml @@ -0,0 +1,9 @@ +--- + +- name: Uninstall node + ansible.builtin.include_tasks: + file: commands/uninstall.yml + +- name: Install node + ansible.builtin.include_tasks: + file: commands/install.yml diff --git a/depin/services/roles/avalanche/tasks/commands/restart.yml b/depin/services/roles/avalanche/tasks/commands/restart.yml new file mode 100644 index 00000000..4cafb5bf --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/restart.yml @@ -0,0 +1,44 @@ +--- + +- name: Get current service info + become: true + ansible.builtin.systemd: + name: "avalanchego" + register: _avalanchego_before + +- name: Set current PID + ansible.builtin.set_fact: + _avalanche_pid: "{{ _avalanchego_before['status']['ExecMainPID'] }}" + +- name: Restart node + become: true + ansible.builtin.service: + name: avalanchego.service + state: restarted + +- name: Assert + when: "'molecule' in groups" + block: + - name: Get new service info + become: true + ansible.builtin.systemd: + name: "avalanchego" + register: _avalanchego_after + + - name: Debug new service status + debug: + msg: + - "Old PID: {{ _avalanche_pid }}" + - "New PID: {{ _avalanchego_after['status']['ExecMainPID'] }}" + - "Active State: {{ _avalanchego_after['status']['ActiveState'] }}" + + - name: Check service + ansible.builtin.assert: + that: + - _avalanchego_after['status']['ActiveState'] == 'active' + - _avalanche_pid | int != _avalanchego_after['status']['ExecMainPID'] | int + quiet: true + + - name: Update PID + ansible.builtin.set_fact: + _avalanche_pid: "{{ _avalanchego_after['status']['ExecMainPID'] }}" diff --git a/depin/services/roles/avalanche/tasks/commands/start.yml b/depin/services/roles/avalanche/tasks/commands/start.yml new file mode 100644 index 00000000..4a3461ef --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/start.yml @@ -0,0 +1,10 @@ +# tasks/commands/start.yml + +--- +- name: Start Avalanche node + become: true + ansible.builtin.service: + name: avalanchego.service + enabled: true + daemon_reload: true + state: started diff --git a/depin/services/roles/avalanche/tasks/commands/stop.yml b/depin/services/roles/avalanche/tasks/commands/stop.yml new file mode 100644 index 00000000..9908d3d0 --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/stop.yml @@ -0,0 +1,28 @@ +--- +## stop service + +- name: Stop node + become: true + ansible.builtin.service: + name: avalanchego.service + enabled: false + state: stopped + +- name: Assert + when: "'molecule' in groups" + block: + - name: Get service info + become: true + ansible.builtin.systemd: + name: "avalanchego" + register: _avalanchego + + - name: Debug service status + debug: + var: _avalanchego.status + + - name: Check service + ansible.builtin.assert: + that: + - _avalanchego['status']['ActiveState'] == 'inactive' + quiet: true diff --git a/depin/services/roles/avalanche/tasks/commands/uninstall.yml b/depin/services/roles/avalanche/tasks/commands/uninstall.yml new file mode 100644 index 00000000..19079e71 --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/uninstall.yml @@ -0,0 +1,53 @@ +--- + +- name: Disable systemd service + ansible.builtin.include_tasks: + file: commands/stop.yml + +- name: Uninstall metrics + vars: + depin_cmd: uninstall + ansible.builtin.include_tasks: + file: tasks/metrics.yml + +- name: Delete content & directory + become: true + ansible.builtin.file: + state: absent + path: "{{ item }}" + loop: + - /etc/systemd/system/avalanchego.service + - /usr/local/bin/avalanchego + - /opt/avalanche/{{ avalanche_version }} + loop_control: + loop_var: item + +- name: Verify that the AvalancheGo service file is removed + ansible.builtin.stat: + path: /etc/systemd/system/avalanchego.service + register: avalanchego_service_file + +- name: Assert that the AvalancheGo service file does not exist + ansible.builtin.assert: + that: + - not avalanchego_service_file.stat.exists + +- name: Verify that the AvalancheGo binary is removed + ansible.builtin.stat: + path: /usr/local/bin/avalanchego + register: avalanchego_binary + +- name: Assert that the AvalancheGo binary does not exist + ansible.builtin.assert: + that: + - not avalanchego_binary.stat.exists + +- name: Verify that the AvalancheGo program directory is removed + ansible.builtin.stat: + path: /opt/avalanche/{{ avalanche_version }} + register: avalanchego_directory + +- name: Assert that the AvalancheGo program directory does not exist + ansible.builtin.assert: + that: + - not avalanchego_directory.stat.exists diff --git a/depin/services/roles/avalanche/tasks/commands/update.yml b/depin/services/roles/avalanche/tasks/commands/update.yml new file mode 100644 index 00000000..aa108e70 --- /dev/null +++ b/depin/services/roles/avalanche/tasks/commands/update.yml @@ -0,0 +1,19 @@ +--- + +- name: Uninstall node + ansible.builtin.include_tasks: + file: commands/uninstall.yml + +- name: Install node + ansible.builtin.include_tasks: + file: commands/install.yml + +- name: Get AvalancheGo version + ansible.builtin.command: /usr/local/bin/avalanchego --version + register: avalanchego_version_output + changed_when: false + +- name: Assert that AvalancheGo is updated to version {{ avalanche_version }} + ansible.builtin.assert: + that: + - "'avalanchego version {{ avalanche_version }}' in avalanchego_version_output.stdout" diff --git a/depin/services/roles/avalanche/tasks/main.yml b/depin/services/roles/avalanche/tasks/main.yml new file mode 100644 index 00000000..9699a270 --- /dev/null +++ b/depin/services/roles/avalanche/tasks/main.yml @@ -0,0 +1,10 @@ +# tasks/main.yml + +--- +- name: Avalanche {{ avalanche_cmd }} + ansible.builtin.include_tasks: + file: commands/{{ avalanche_cmd }}.yml + +- name: Configure metrics + ansible.builtin.include_tasks: + file: metrics.yml diff --git a/depin/services/roles/avalanche/tasks/metrics.yml b/depin/services/roles/avalanche/tasks/metrics.yml new file mode 100644 index 00000000..2e3fecae --- /dev/null +++ b/depin/services/roles/avalanche/tasks/metrics.yml @@ -0,0 +1,65 @@ +# tasks/metrics.yml + +--- +- name: Install Avalanche metrics + vars: + _name: "{{ role_name | replace('_', '-') }}" + _metrics_port: avalanche_metrics_port + when: avalanche_cmd != 'uninstall' + become: true + block: + - name: Ensure directory exists + ansible.builtin.file: + path: /etc/deeep-network/collectors + state: directory + mode: '0755' + recurse: true + + - name: Install script + ansible.builtin.template: + src: templates/collector.py.j2 + dest: /etc/deeep-network/collectors/{{ _name }}.py + mode: '0744' + + - name: Install service + ansible.builtin.template: + src: templates/metrics.service.j2 + dest: /etc/systemd/system/collector-{{ _name }}.service + mode: '0644' + + - name: Update prometheus.conf + vars: + _block: | + - name: {{ _name }} + url: 'http://127.0.0.1:{{ vars[_metrics_port] }}/metrics' + ansible.builtin.blockinfile: + path: /etc/netdata/go.d/prometheus.conf + append_newline: true + block: "{{ _block | indent(4, first=true) }}" + + - name: Start service + ansible.builtin.systemd_service: + name: collector-{{ _name }} + enabled: true + daemon_reload: true + state: started + +- name: Uninstall Avalanche metrics + vars: + _name: "{{ role_name | replace('_', '-') }}" + when: avalanche_cmd == 'uninstall' + become: true + block: + - name: Stop service + ansible.builtin.systemd_service: + name: collector-{{ _name }} + enabled: false + state: stopped + + - name: Remove files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/systemd/system/collector-{{ _name }}.service + - /etc/deeep-network/collectors/{{ _name }}.py diff --git a/depin/services/roles/avalanche/templates/collector.py.j2 b/depin/services/roles/avalanche/templates/collector.py.j2 new file mode 100644 index 00000000..46fc9eab --- /dev/null +++ b/depin/services/roles/avalanche/templates/collector.py.j2 @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +import os +import socket +import time +import subprocess +from subprocess import check_output +import json +from pathlib import Path + +from prometheus_client import start_http_server, REGISTRY, GC_COLLECTOR, PLATFORM_COLLECTOR, PROCESS_COLLECTOR +from prometheus_client.core import GaugeMetricFamily +from prometheus_client.registry import Collector + +try: + from utils_lxd import lxd_get +except ImportError as imp_exc: + LXD_UTILS_IMPORT_ERROR = imp_exc +else: + LXD_UTILS_IMPORT_ERROR = None + +class MetricsCollector(Collector): + """Collector for Avalanche Node information""" + def total_uptime_count(self, autonomi_service_date): + total_uptime = 0 + for x in autonomi_service_date: + state_uptime_list = autonomi_service_date[x].split(":") + hours = 0 + if len(state_uptime_list) <= 2: + minutes = int(state_uptime_list[0]) + seconds = int(state_uptime_list[1]) + else: + hours = int(state_uptime_list[0]) + minutes = int(state_uptime_list[1]) + seconds = int(state_uptime_list[2]) + total_uptime += seconds + minutes * 60 + hours * 3600 + return total_uptime + + def collect(self): + result = [] + service_metrics_collected = 1 + + device = 'molecule' + # Fetch device hostname - LXD specific feature + if LXD_UTILS_IMPORT_ERROR is None: + device_info = lxd_get("1.0/config/user.location") + if 'value' in device_info: + device = device_info['value'] + + hostname = socket.gethostname() + service = Path(__file__).stem + + _labels = ['device', 'instance', 'service'] + _label_values = [device, hostname, service] + + # Placeholder values for state_uptime and total_uptime + state_uptime = 12345 # Replace with actual calculation + total_uptime = 67890 # Replace with actual calculation + + success = GaugeMetricFamily( + 'service_metrics_collected', + 'Service metrics collected successfully', + labels=_labels + ) + success.add_metric(_label_values, value=service_metrics_collected) + + current_state_uptime = GaugeMetricFamily( + 'current_state_uptime', + 'Seconds in current state', + labels=_labels + ) + current_state_uptime.add_metric(_label_values, value=state_uptime) + + total_uptime_metric = GaugeMetricFamily( + 'total_uptime', + 'Seconds in last 24 hours', + labels=_labels + ) + total_uptime_metric.add_metric(_label_values, value=total_uptime) + + result.extend([success, current_state_uptime, total_uptime_metric]) + + return result + +if __name__ == "__main__": + REGISTRY.unregister(GC_COLLECTOR) + REGISTRY.unregister(PLATFORM_COLLECTOR) + REGISTRY.unregister(PROCESS_COLLECTOR) + REGISTRY.register(MetricsCollector()) + + # Use the file name stem as the role name + role_name = Path(__file__).stem + + # Set the metrics port (default to 8000 or fetch from environment variable) + metrics_port = int(os.getenv(f'{role_name.upper()}_METRICS_PORT', 8000)) + start_http_server(metrics_port) + + while True: + time.sleep(30) diff --git a/depin/services/roles/avalanche/templates/config.json.j2 b/depin/services/roles/avalanche/templates/config.json.j2 new file mode 100644 index 00000000..d0f243c5 --- /dev/null +++ b/depin/services/roles/avalanche/templates/config.json.j2 @@ -0,0 +1,6 @@ +{ + "state-sync-enabled": {{ avalanche_state_sync_enabled | lower }}{% if avalanche_archival_mode %},{% endif %} + {% if avalanche_archival_mode %} + "pruning-enabled": false + {% endif %} +} diff --git a/depin/services/roles/avalanche/templates/metrics.service.j2 b/depin/services/roles/avalanche/templates/metrics.service.j2 new file mode 100644 index 00000000..5bbe69a3 --- /dev/null +++ b/depin/services/roles/avalanche/templates/metrics.service.j2 @@ -0,0 +1,15 @@ +{{ ansible_managed | comment }} + +[Unit] +Description=Prometheus collector - {{ _name }} +After=network-online.target + +[Service] +Type=simple +User=nerdnode +ExecStart=sudo /opt/pipx/venvs/ansible-core/bin/python3 /etc/deeep-network/collectors/{{ _name }}.py +KillSignal=SIGINT +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/depin/services/roles/avalanche/templates/node.json.j2 b/depin/services/roles/avalanche/templates/node.json.j2 new file mode 100644 index 00000000..c345de5f --- /dev/null +++ b/depin/services/roles/avalanche/templates/node.json.j2 @@ -0,0 +1,22 @@ +{ +{% if avalanche_rpc_public %} + "http-host": "", +{% endif %} +{% if avalanche_admin_api_enabled %} + "api-admin-enabled": true, +{% endif %} +{% if avalanche_index_enabled %} + "index-enabled": true, +{% endif %} +{% if avalanche_network_id != 'mainnet' %} + "network-id": "{{ avalanche_network_id }}", +{% endif %} +{% if avalanche_db_dir %} + "db-dir": "{{ avalanche_db_dir }}", +{% endif %} +{% if avalanche_ip_mode == 'dynamic' %} + "public-ip-resolution-service": "opendns" +{% else %} + "public-ip": "{{ avalanche_public_ip }}" +{% endif %} +} diff --git a/depin/services/roles/avalanche/templates/systemd.service.j2 b/depin/services/roles/avalanche/templates/systemd.service.j2 new file mode 100644 index 00000000..4540a5e8 --- /dev/null +++ b/depin/services/roles/avalanche/templates/systemd.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=AvalancheGo node service +After=network.target + +[Service] +User={{ avalanche_user }} +Group={{ avalanche_group }} +Type=simple +ExecStart={{ avalanche_install_dir }}/avalanchego --config-file={{ avalanche_config_dir }}/configs/node.json +LimitNOFILE=32768 +Restart=always +RestartSec=1 + +[Install] +WantedBy=multi-user.target diff --git a/molecule/default/tasks/vars/avalanche.yml b/molecule/default/tasks/vars/avalanche.yml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/molecule/default/tasks/vars/avalanche.yml @@ -0,0 +1 @@ + diff --git a/molecule/libs/tasks/vars/avalanche.yml b/molecule/libs/tasks/vars/avalanche.yml new file mode 100644 index 00000000..e69de29b