add deps to git
parent
f70527d570
commit
35afd3d71f
@ -0,0 +1,20 @@
|
||||
name: E2E
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the codebase
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8.9
|
||||
- name: Install dependencies
|
||||
run: make deps
|
||||
- name: Test Backup & Restore
|
||||
run: make e2e
|
||||
env:
|
||||
VAULT_KEY: "${{ secrets.VAULT_KEY }}"
|
||||
@ -0,0 +1,18 @@
|
||||
name: Code Health
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the codebase
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8.9
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Test Directory
|
||||
run: ansible-lint roles
|
||||
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
venv
|
||||
@ -0,0 +1,292 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"name": ".",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "LICENSE",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "eb1e623301bf97851c37dc8127635aba0407c91b8583725472905407facc3fb1",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "requirements.txt",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "49b26f38de2b9738843174e6e259cba40e33e50581dd8e2fdf3f881f88e8ace5",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/filter",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/filter/filters.py",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "dcfb6a2c0e2437210beffc1fb8857fa80dc7fda8dde987bc3f627bd14363776e",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "Makefile",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "8e1aa2e50b6c38e0b2fcb86f3f8fd574603ad6d6ad0e25f717670e63a9f1ab33",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/ansible.cfg",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "efb91542404527c3081e347a8833c54a0c6f6f7b975e400dd5c350aef9c2c822",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/playbook.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "a52f9f26b558f05a29a0447f10a1ea7ca96df1b7bc44a31b2727c624c267e3fb",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/docker-compose.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "0a293c4b8f09df4855a04529029205634389596a4d25b249500eac9d1a4cc24c",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/host_vars",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/host_vars/localhost.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "388a868ea1dd4cded00276063b18f221bb3ed43040378fdcd6443dc2240d3db5",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/vault_key.sh",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "48884297731a934b4f0ad2cdb104f53c12341370d2413fa61a62fa3c4024eab4",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/vars",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/vars/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "6c98c057bc941803da7172ff80160a669e987360bad34af5ea53593985f1443a",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/tasks",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/tasks/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "0fcf1fbf3fafdaf129a33876344c1ea07d54ca562c253cc81c5eebc806ff51a9",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/meta",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/meta/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "8e707c10e6614fae3baf7f6199e74ff23dedf7128dd170b8ad9924ed2cd607bc",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/defaults",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_backup/defaults/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "cd31b1ba5dd374feac22fce70bb91bd948a391c27691f8cc49190427dfd360aa",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/vars",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/vars/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "8cbb7ae1ada7a17063d49063a2ded3b3f784a9acc5b0b42a35d417134713a3d2",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/tasks",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/tasks/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "912b5afae6e9183741dd29242e1810ceb32deabb3624f2a2ccc2b1ea8984a45f",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/meta",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/meta/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "8495f3dccdbd037f3ba05f8c1f00d1de836e751b5f76504fa57d6e5c8032ccb0",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/defaults",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/defaults/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "b0ecdc12d3f7814ebfbded52d4db758462c2c69b34635e670ee7ee2322d6c44f",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/handlers",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "roles/docker_s3_volume_restore/handlers/main.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "bda514ba267156bf4b25e378da315e16c28e110fbc4a4033c91e71e8d417bb1b",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "README.md",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "678fe27fb5f532e6c89a7538da7931d64f7d6522a4c857a2de5495afabaeaab3",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".gitignore",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "54c27deac980434757ea7dc177f753710633b9464576e23804c2d5f5bc549288",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".github",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".github/workflows",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".github/workflows/lint.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "aa67d9ff8c695a9c47d18c540186647ef12160ed924608a4adabf4d0bfb5510a",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".github/workflows/e2e.yml",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "c78a395feeca4a7f3751cedd0666ad01c7d4e206ffecaf6928deefd0f8d9b384",
|
||||
"format": 1
|
||||
}
|
||||
],
|
||||
"format": 1
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Cian Hatton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"collection_info": {
|
||||
"namespace": "chatton",
|
||||
"name": "docker_backup",
|
||||
"version": "1.0.0",
|
||||
"authors": [
|
||||
"Cian Hatton cianhatton@protonmail.com"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"tags": [],
|
||||
"description": "A collection of roles which allow backup and restore of docker volumes.",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"license_file": "LICENSE",
|
||||
"dependencies": {},
|
||||
"repository": "https://github.com/chatton/ansible-docker-backup",
|
||||
"documentation": "https://github.com/chatton/ansible-docker-backup#readme",
|
||||
"homepage": "https://github.com/chatton/ansible-docker-backup",
|
||||
"issues": "https://github.com/chatton/ansible-docker-backup/issues"
|
||||
},
|
||||
"file_manifest_file": {
|
||||
"name": "FILES.json",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "4f37593e42d3f2b5b2225b4aade83be41bee2a507b88546d3bbd79e061017173",
|
||||
"format": 1
|
||||
},
|
||||
"format": 1
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
e2e: test clean
|
||||
|
||||
test:
|
||||
cp -r tests/host_vars .
|
||||
cp tests/ansible.cfg .
|
||||
cp tests/docker-compose.yml .
|
||||
cp tests/playbook.yml .
|
||||
ansible-playbook playbook.yml
|
||||
|
||||
clean:
|
||||
rm -r host_vars
|
||||
rm ansible.cfg
|
||||
rm docker-compose.yml
|
||||
rm playbook.yml
|
||||
|
||||
deps:
|
||||
pip install -r requirements.txt
|
||||
@ -0,0 +1,3 @@
|
||||
# Ansible Collection - chatton.docker_backup
|
||||
|
||||
Documentation for the collection.
|
||||
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/python
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
'keep_last': self._keep_last,
|
||||
}
|
||||
|
||||
"""
|
||||
_keep_last keeps the last n items of a list of lists
|
||||
"""
|
||||
def _keep_last(self, list_of_lists, num_to_keep):
|
||||
lists_to_return = []
|
||||
for list_items in list_of_lists:
|
||||
lists_to_return.append(list_items[0:-num_to_keep])
|
||||
return lists_to_return
|
||||
@ -0,0 +1,37 @@
|
||||
ansible==6.3.0
|
||||
ansible-compat==2.2.0
|
||||
ansible-core==2.13.3
|
||||
ansible-lint==6.5.2
|
||||
attrs==22.1.0
|
||||
black==22.8.0
|
||||
bracex==2.3.post1
|
||||
cffi==1.15.1
|
||||
click==8.1.3
|
||||
commonmark==0.9.1
|
||||
cryptography==37.0.4
|
||||
enrich==1.2.7
|
||||
filelock==3.8.0
|
||||
importlib-resources==5.9.0
|
||||
Jinja2==3.1.2
|
||||
jsonschema==4.15.0
|
||||
MarkupSafe==2.1.1
|
||||
mypy-extensions==0.4.3
|
||||
packaging==21.3
|
||||
pathspec==0.10.1
|
||||
pkgutil_resolve_name==1.3.10
|
||||
platformdirs==2.5.2
|
||||
pycparser==2.21
|
||||
Pygments==2.13.0
|
||||
pyparsing==3.0.9
|
||||
pyrsistent==0.18.1
|
||||
PyYAML==6.0
|
||||
resolvelib==0.8.1
|
||||
rich==12.5.1
|
||||
ruamel.yaml==0.17.21
|
||||
ruamel.yaml.clib==0.2.6
|
||||
subprocess-tee==0.3.5
|
||||
tomli==2.0.1
|
||||
typing_extensions==4.3.0
|
||||
wcmatch==8.4
|
||||
yamllint==1.27.1
|
||||
zipp==3.8.1
|
||||
@ -0,0 +1,15 @@
|
||||
---
|
||||
# defaults file for chatton.docker_s3_backup
|
||||
|
||||
# the number of backups of the same volume which should be kept.
|
||||
# Any excess will be deleted.
|
||||
docker_backup_retain_count: 3
|
||||
# the backup directory where backups are stored on the host machine.
|
||||
# these will be uploaded to S3.
|
||||
docker_backup_aws_s3_region: "us-east-1"
|
||||
docker_backup_aws_s3_bucket: "backups"
|
||||
docker_backup_host_backup_directory: ""
|
||||
docker_backup_aws_s3_url: ""
|
||||
docker_backup_aws_s3_aws_access_key: ""
|
||||
docker_backup_aws_s3_aws_secret_key: ""
|
||||
docker_backup_aws_s3_permissions: []
|
||||
@ -0,0 +1,11 @@
|
||||
galaxy_info:
|
||||
author: Cian Hatton
|
||||
description: Role which backs up a dockver volume to s3.
|
||||
license: MIT
|
||||
min_ansible_version: "2.1"
|
||||
galaxy_tags: []
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions:
|
||||
- all
|
||||
dependencies: []
|
||||
@ -0,0 +1,85 @@
|
||||
---
|
||||
# https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_module.html#ansible-collections-community-docker-docker-container-module
|
||||
# https://docs.docker.com/storage/volumes/#backup-restore-or-migrate-data-volumes
|
||||
|
||||
- name: Determine backup timestamp.
|
||||
ansible.builtin.set_fact: backup_time="{{ ansible_date_time.iso8601 }}"
|
||||
|
||||
- name: Install Python dependencies
|
||||
ansible.builtin.pip:
|
||||
name:
|
||||
- docker
|
||||
- boto3
|
||||
|
||||
- name: Stop a container
|
||||
community.docker.docker_container:
|
||||
name: "{{ container_backup }}"
|
||||
state: stopped
|
||||
|
||||
- name: Get container details
|
||||
docker_container_info:
|
||||
name: "{{ container_backup }}"
|
||||
register: result
|
||||
|
||||
- name: Extract only the volume mounts (not bind mounts)
|
||||
ansible.builtin.set_fact: volume_mounts="{{ result.container.Mounts | selectattr("Type", "equalto", "volume") }}"
|
||||
|
||||
- name: Create Backup of Container Volumes
|
||||
community.docker.docker_container:
|
||||
name: "backup-container-{{ item.Name }}-{{ 10 | random }}"
|
||||
image: ubuntu
|
||||
command: "tar -czvf /backups/{{ item.Name }}-{{ backup_time }}.tar.gz /data"
|
||||
auto_remove: true
|
||||
detach: false # block until this container exists.
|
||||
state: started
|
||||
volumes:
|
||||
- "{{ item.Name }}:/data"
|
||||
- "{{ docker_backup_host_backup_directory }}:/backups"
|
||||
with_items: "{{ volume_mounts }}"
|
||||
|
||||
- name: Start the container
|
||||
community.docker.docker_container:
|
||||
name: "{{ container_backup }}"
|
||||
state: started
|
||||
|
||||
- name: Upload backups to S3
|
||||
register: upload_result
|
||||
amazon.aws.aws_s3:
|
||||
s3_url: "{{ docker_backup_aws_s3_url }}"
|
||||
bucket: "{{ docker_backup_aws_s3_bucket }}"
|
||||
object: "{{ item.Name }}/{{ item.Name }}-{{ backup_time }}.tar.gz"
|
||||
src: "{{ docker_backup_host_backup_directory }}/{{ item.Name }}-{{ backup_time }}.tar.gz"
|
||||
aws_access_key: "{{ docker_backup_aws_s3_aws_access_key }}"
|
||||
aws_secret_key: "{{ docker_backup_aws_s3_aws_secret_key }}"
|
||||
region: "{{ docker_backup_aws_s3_region }}"
|
||||
mode: put
|
||||
permission: "{{ docker_backup_aws_s3_permissions }}"
|
||||
with_items: "{{ volume_mounts }}"
|
||||
|
||||
# try and find latest volume based on the name.
|
||||
- name: Fetch Volumes From S3.
|
||||
amazon.aws.aws_s3:
|
||||
bucket: "{{ docker_backup_aws_s3_bucket }}"
|
||||
mode: list
|
||||
prefix: "{{ item.Name }}/{{ item.Name }}"
|
||||
aws_access_key: "{{ docker_backup_aws_s3_aws_access_key }}"
|
||||
aws_secret_key: "{{ docker_backup_aws_s3_aws_secret_key }}"
|
||||
region: "{{ docker_backup_aws_s3_region }}"
|
||||
s3_url: "{{ docker_backup_aws_s3_url }}"
|
||||
register: s3_list_outputs
|
||||
with_items: "{{ volume_mounts }}"
|
||||
|
||||
- name: Find keys to delete.
|
||||
ansible.builtin.set_fact:
|
||||
s3_keys_to_delete: "{{ s3_list_outputs.results | map(attribute='s3_keys') | keep_last(docker_backup_retain_count) | flatten }}"
|
||||
|
||||
- name: Delete old backups.
|
||||
amazon.aws.aws_s3:
|
||||
bucket: "{{ docker_backup_aws_s3_bucket }}"
|
||||
aws_access_key: "{{ docker_backup_aws_s3_aws_access_key }}"
|
||||
aws_secret_key: "{{ docker_backup_aws_s3_aws_secret_key }}"
|
||||
region: "{{ docker_backup_aws_s3_region }}"
|
||||
s3_url: "{{ docker_backup_aws_s3_url }}"
|
||||
object: "{{ item }}"
|
||||
mode: delobj
|
||||
with_items: "{{ s3_keys_to_delete }}"
|
||||
@ -0,0 +1,2 @@
|
||||
---
|
||||
# vars file for chatton.docker_backup
|
||||
@ -0,0 +1,25 @@
|
||||
---
|
||||
# defaults file for docker_s3_volume_restore
|
||||
|
||||
# forces a revert to the volume.
|
||||
docker_backup_restore_force: false
|
||||
# specify docker_backup_restore_latest_s3_key true to automatically determine the latest
|
||||
# backup in the s3 backup. The format which is expected has the prefix of volume_name/volume_name_*
|
||||
# this is the format the the "docker_s3_backup" role updloads them with.
|
||||
docker_backup_restore_latest_s3_key: false
|
||||
docker_backup_s3_restores: []
|
||||
# docker_backup_s3_restores:
|
||||
# - volume_name: "linkding_data"
|
||||
# s3_key: "linkding_data/linkding_data-2022-09-01T21:32:54Z.tar.gz"
|
||||
|
||||
# dynamically find the latest linkding_data backup.
|
||||
# docker_backup_restore_latest_s3_key: true
|
||||
# docker_backup_s3_restores:
|
||||
# - volume_name: "linkding_data"
|
||||
|
||||
docker_backup_aws_s3_region: "us-east-1"
|
||||
docker_backup_aws_s3_bucket: "backups"
|
||||
docker_backup_aws_s3_url: ""
|
||||
docker_backup_aws_s3_aws_access_key: ""
|
||||
docker_backup_aws_s3_aws_secret_key: ""
|
||||
docker_backup_aws_s3_permissions: []
|
||||
@ -0,0 +1,2 @@
|
||||
---
|
||||
# handlers file for docker_s3_volume_restore
|
||||
@ -0,0 +1,11 @@
|
||||
galaxy_info:
|
||||
author: Cian Hatton
|
||||
description: Role which restores a dockver volume from s3.
|
||||
license: MIT
|
||||
min_ansible_version: "2.1"
|
||||
galaxy_tags: []
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions:
|
||||
- all
|
||||
dependencies: []
|
||||
@ -0,0 +1,107 @@
|
||||
---
|
||||
# https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_module.html#ansible-collections-community-docker-docker-container-module
|
||||
# https://docs.docker.com/storage/volumes/#backup-restore-or-migrate-data-volumes
|
||||
|
||||
- name: Ensure Volume.
|
||||
docker_volume:
|
||||
name: "{{ item.volume_name }}"
|
||||
state: present
|
||||
register: volume_out
|
||||
with_items: "{{ docker_backup_s3_restores }}"
|
||||
|
||||
- name: Determine if backup is needed.
|
||||
ansible.builtin.set_fact: should_perform_backup="{{ docker_backup_restore_force == true or volume_out.changed == true }}"
|
||||
|
||||
- name: End play as no backup is needed.
|
||||
ansible.builtin.meta: end_play
|
||||
when: not should_perform_backup
|
||||
|
||||
# try and find latest volume based on the name.
|
||||
- name: Find latest s3 version.
|
||||
when: docker_backup_restore_latest_s3_key
|
||||
amazon.aws.aws_s3:
|
||||
bucket: "{{ docker_backup_aws_s3_bucket }}"
|
||||
mode: list
|
||||
prefix: "{{ item.volume_name }}/{{ item.volume_name }}"
|
||||
aws_access_key: "{{ docker_backup_aws_s3_aws_access_key }}"
|
||||
aws_secret_key: "{{ docker_backup_aws_s3_aws_secret_key }}"
|
||||
region: "{{ docker_backup_aws_s3_region }}"
|
||||
s3_url: "{{ docker_backup_aws_s3_url }}"
|
||||
register: s3_list_output
|
||||
with_items: "{{ docker_backup_s3_restores }}"
|
||||
|
||||
- name: Extract S3 keys for container.
|
||||
when: docker_backup_restore_latest_s3_key
|
||||
ansible.builtin.set_fact: container_s3_keys="{{ container_s3_keys | default([]) + [item.s3_keys | last] }}"
|
||||
with_items: "{{ s3_list_output.results }}"
|
||||
|
||||
- name: Extract volume names.
|
||||
ansible.builtin.set_fact: volume_names="{{ docker_backup_s3_restores | map(attribute='volume_name') }}"
|
||||
when: docker_backup_restore_latest_s3_key
|
||||
|
||||
- name: Merge volume names and S3 keys.
|
||||
ansible.builtin.set_fact:
|
||||
docker_backup_s3_restores_new: "{{ docker_backup_s3_restores_new | default([]) + [ {'volume_name': item.0, 's3_key': item.1} ] }}"
|
||||
when: docker_backup_restore_latest_s3_key
|
||||
with_together:
|
||||
- "{{ volume_names }}"
|
||||
- "{{ container_s3_keys }}"
|
||||
|
||||
- name: Set volumes to restore.
|
||||
ansible.builtin.set_fact:
|
||||
docker_backup_s3_restores_new: "{{ docker_backup_s3_restores }}"
|
||||
when: not docker_backup_restore_latest_s3_key
|
||||
|
||||
- name: Create directories for /tmp file.
|
||||
ansible.builtin.file:
|
||||
path: '/tmp/{{ item.s3_key | dirname }}'
|
||||
state: directory
|
||||
mode: '0755'
|
||||
with_items: '{{ docker_backup_s3_restores_new }}'
|
||||
|
||||
- name: Download archive from S3.
|
||||
amazon.aws.aws_s3:
|
||||
bucket: "{{ docker_backup_aws_s3_bucket }}"
|
||||
object: "{{ item.s3_key }}"
|
||||
aws_access_key: "{{ docker_backup_aws_s3_aws_access_key }}"
|
||||
aws_secret_key: "{{ docker_backup_aws_s3_aws_secret_key }}"
|
||||
region: "{{ docker_backup_aws_s3_region }}"
|
||||
s3_url: "{{ docker_backup_aws_s3_url }}"
|
||||
mode: get
|
||||
dest: "/tmp/{{ item.s3_key }}"
|
||||
register: get_out
|
||||
with_items: "{{ docker_backup_s3_restores_new }}"
|
||||
|
||||
- name: Remove contents of volumes.
|
||||
community.docker.docker_container:
|
||||
name: "restore-container-{{ item.volume_name }}-{{ 10 | random }}"
|
||||
image: ubuntu
|
||||
command: "rm -rf ./*"
|
||||
auto_remove: true
|
||||
detach: false # block until this container exists.
|
||||
state: started
|
||||
# start inside the directory we want to wipe
|
||||
working_dir: "/data"
|
||||
volumes:
|
||||
- "{{ item.volume_name }}:/data"
|
||||
with_items: "{{ docker_backup_s3_restores_new }}"
|
||||
|
||||
- name: Restore contents of volumes
|
||||
community.docker.docker_container:
|
||||
name: "restore-container-{{ item.volume_name }}-{{ 10 | random }}"
|
||||
image: ubuntu
|
||||
# extract the tar into the volume.
|
||||
command: "tar xvf /tmp/{{ item.s3_key }} -C /data --strip-components 1"
|
||||
auto_remove: true
|
||||
detach: false # block until this container exists.
|
||||
state: started
|
||||
volumes:
|
||||
- "{{ item.volume_name }}:/data"
|
||||
- /tmp:/tmp
|
||||
with_items: "{{ docker_backup_s3_restores_new }}"
|
||||
|
||||
- name: Remove uploaded files from /tmp
|
||||
ansible.builtin.file:
|
||||
path: '/tmp/{{ item.s3_key }}'
|
||||
state: absent
|
||||
with_items: '{{ docker_backup_s3_restores_new }}'
|
||||
@ -0,0 +1,2 @@
|
||||
---
|
||||
# vars file for docker_s3_volume_restore
|
||||
@ -0,0 +1,4 @@
|
||||
[defaults]
|
||||
roles_path = roles
|
||||
vault_password_file=tests/vault_key.sh
|
||||
filter_plugins = plugins/filter
|
||||
@ -0,0 +1,18 @@
|
||||
---
|
||||
version: '3.2'
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ce
|
||||
container_name: portainer
|
||||
ports:
|
||||
- 9000:9000
|
||||
volumes:
|
||||
- portainer_data:/data
|
||||
- some_volume:/some_dir
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
volumes:
|
||||
some_volume:
|
||||
portainer_data:
|
||||
external: true
|
||||
name: portainer_data
|
||||
@ -0,0 +1,23 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
31343237643365393335643262646139363037616365623238316536313238386237353436643738
|
||||
3332376365393138633962373132633562363035396161650a323039646238373162333366353533
|
||||
37333238643437623538343630393065313463313038616538613838366638636433313637313630
|
||||
3733623838373864320a373636303030373737336265353165373463306233333033393266343838
|
||||
31323634313531343262323561636438326166343461373133393166303630303865316661356165
|
||||
64366435343933396131313665336437363234393239663831363033356433383330323964613833
|
||||
36346436383739623163343130376465333465316139303437666333386633313132336234383936
|
||||
65353437336637353739373837343766333266396263396562366463626332363061383435353132
|
||||
34616134396136333266323930343866663332373864623537623765303435366331613466376137
|
||||
63346337303461623036306362306235366365646137316165376634316230396239623132363337
|
||||
35363932663361313533663436633532313732646564663463393233316231623361336332336135
|
||||
39356338363336336231643661313062303734316539653031313630363866303464643438653035
|
||||
37393039623961386539303235636562653130343237336332643639346631326633363366373466
|
||||
62356536356664353466383131306664653132393837663635366466613665626535323930366637
|
||||
38646263326264313363386634363834626638383563346361386165323430383266646631626362
|
||||
64303263383138353739656534623734623638653438353666623464656461316636626564326536
|
||||
34616163626539383265353963333734313363343162663434356337393266313637323732346231
|
||||
33373835373465666637663330653337373130373732303632326530336132333236313466653239
|
||||
30383632363337333833666132363563363361623865616134613538373439353836346366353065
|
||||
34356134633038653839333430313738613531653634333430373635373239653362393461306330
|
||||
35326532303432316636666134353534626139316331333538356165313965613739653665616237
|
||||
3165353731626130666639346263333865316362623134373463
|
||||
@ -0,0 +1,106 @@
|
||||
---
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Install Docker Module for Python
|
||||
pip:
|
||||
name:
|
||||
- docker
|
||||
- docker-compose
|
||||
- boto3
|
||||
|
||||
- name: Remove Portainer
|
||||
docker_compose:
|
||||
project_src: 'tests'
|
||||
state: absent
|
||||
|
||||
- name: Remove portainer volume
|
||||
docker_volume:
|
||||
name: portainer_data
|
||||
state: absent
|
||||
|
||||
- name: Create portainer volume
|
||||
docker_volume:
|
||||
name: portainer_data
|
||||
state: present
|
||||
|
||||
- name: Deploy Portainer
|
||||
docker_compose:
|
||||
project_src: 'tests'
|
||||
state: present
|
||||
|
||||
|
||||
- name: Portainer | Wait for ready
|
||||
uri:
|
||||
url: http://localhost:9000
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: result
|
||||
until: result.status == 200
|
||||
retries: 60
|
||||
delay: 1
|
||||
|
||||
- debug: msg="{{result}}"
|
||||
|
||||
- name: Register Portainer Admin User
|
||||
uri:
|
||||
url: http://localhost:9000/api/users/admin/init
|
||||
method: POST
|
||||
body:
|
||||
Username: admin
|
||||
Password: "adminadminadmin"
|
||||
status_code: 200
|
||||
body_format: json
|
||||
register: result
|
||||
until: result.status == 200
|
||||
retries: 60
|
||||
delay: 1
|
||||
|
||||
- name: Backup Portainer
|
||||
include_role:
|
||||
name: docker_s3_backup
|
||||
vars:
|
||||
container_backup: portainer
|
||||
|
||||
- name: Remove Portainer
|
||||
docker_compose:
|
||||
project_src: 'tests'
|
||||
state: absent
|
||||
|
||||
- name: Remove portainer volume
|
||||
docker_volume:
|
||||
name: portainer_data
|
||||
state: absent
|
||||
|
||||
- name: Restore Portainer Volume
|
||||
include_role:
|
||||
name: docker_s3_volume_restore
|
||||
vars:
|
||||
docker_backup_restore_latest_s3_key: true
|
||||
docker_backup_s3_restores:
|
||||
- volume_name: portainer_data
|
||||
|
||||
- name: Deploy Portainer
|
||||
docker_compose:
|
||||
project_src: 'tests'
|
||||
state: present
|
||||
|
||||
- name: Auth as old user
|
||||
uri:
|
||||
url: http://localhost:9000/api/auth
|
||||
method: POST
|
||||
body:
|
||||
Username: admin
|
||||
Password: "adminadminadmin"
|
||||
status_code: 200
|
||||
body_format: json
|
||||
register: result
|
||||
until: result.status == 200
|
||||
retries: 60
|
||||
delay: 1
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result.status == 200
|
||||
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
echo "${VAULT_KEY}"
|
||||
@ -0,0 +1,4 @@
|
||||
.idea
|
||||
venv
|
||||
.pytest_cache
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"name": ".",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "LICENSE",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "eb1e623301bf97851c37dc8127635aba0407c91b8583725472905407facc3fb1",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "requirements.txt",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "15f22e3e2654ae2de7af40c232ecef6b73718c595f4d7632820feb31bcbb51e7",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/README.md",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "23e9939164cad964c2338b8059e4d3def72eef9523e32594503efd50960fcae4",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/module_utils",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/module_utils/portainer.py",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "95616af2c5bde69ee194b26473cc55a2c0dddf51c62d8886b9285cc4e21117b9",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/modules",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "plugins/modules/portainer_stack.py",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "8d7c6e609e3d6d1764768c479a07375b8b43d65a9fc30e7f8cefe1bc48fe7d2b",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/unit",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/unit/plugins",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/unit/plugins/modules",
|
||||
"ftype": "dir",
|
||||
"chksum_type": null,
|
||||
"chksum_sha256": null,
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "tests/unit/plugins/modules/test_portainer_stack.py",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "557f9717bcb5a666c0b93fdc97525d6adae84bc84e3d307cbb33af1bc1bb066d",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": "README.md",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "294bfca64fb7c1480fb6b4f0fb6cfc1e73ce3ea13f37e34ddeb403a3fd87aabd",
|
||||
"format": 1
|
||||
},
|
||||
{
|
||||
"name": ".gitignore",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "f6a51f14c4c681963ae1e30806d6868fe6677d47f0a490473521df7497d713dc",
|
||||
"format": 1
|
||||
}
|
||||
],
|
||||
"format": 1
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Cian Hatton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"collection_info": {
|
||||
"namespace": "chatton",
|
||||
"name": "portainer",
|
||||
"version": "1.0.0",
|
||||
"authors": [
|
||||
"Cian Hatton"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"tags": [],
|
||||
"description": "your collection description",
|
||||
"license": [
|
||||
"GPL-2.0-or-later"
|
||||
],
|
||||
"license_file": null,
|
||||
"dependencies": {},
|
||||
"repository": "https://github.com/chatton/ansible-portainer",
|
||||
"documentation": "http://docs.example.com",
|
||||
"homepage": "http://example.com",
|
||||
"issues": "https://github.com/chatton/ansible-portainer/issues"
|
||||
},
|
||||
"file_manifest_file": {
|
||||
"name": "FILES.json",
|
||||
"ftype": "file",
|
||||
"chksum_type": "sha256",
|
||||
"chksum_sha256": "af38f629ca55ff7f83f84e54ee9496e2fdcd93eb6afce03d79f12dd7d68a5153",
|
||||
"format": 1
|
||||
},
|
||||
"format": 1
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
# ansible-portainer
|
||||
@ -0,0 +1,31 @@
|
||||
# Collections Plugins Directory
|
||||
|
||||
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
|
||||
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
|
||||
would contain module utils and modules respectively.
|
||||
|
||||
Here is an example directory of the majority of plugins currently supported by Ansible:
|
||||
|
||||
```
|
||||
└── plugins
|
||||
├── action
|
||||
├── become
|
||||
├── cache
|
||||
├── callback
|
||||
├── cliconf
|
||||
├── connection
|
||||
├── filter
|
||||
├── httpapi
|
||||
├── inventory
|
||||
├── lookup
|
||||
├── module_utils
|
||||
├── modules
|
||||
├── netconf
|
||||
├── shell
|
||||
├── strategy
|
||||
├── terminal
|
||||
├── test
|
||||
└── vars
|
||||
```
|
||||
|
||||
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.13/plugins/plugins.html).
|
||||
@ -0,0 +1,59 @@
|
||||
import requests
|
||||
|
||||
|
||||
def _query_params_to_string(params):
|
||||
s = "?"
|
||||
for k, v in params.items():
|
||||
s += f"&{k}={v}"
|
||||
return s
|
||||
|
||||
|
||||
class PortainerClient:
|
||||
def __init__(self, base_url, endpoint):
|
||||
self.endpoint = endpoint
|
||||
self.base_url = base_url
|
||||
self.token = ""
|
||||
self.headers = {}
|
||||
|
||||
def login(self, username, password):
|
||||
payload = {
|
||||
"Username": username,
|
||||
"Password": password,
|
||||
}
|
||||
auth_url = f"{self.base_url}/api/auth"
|
||||
resp = requests.post(auth_url, json=payload)
|
||||
resp.raise_for_status()
|
||||
self.token = resp.json()["jwt"]
|
||||
self.headers = {"Authorization": f"Bearer {self.token}"}
|
||||
|
||||
def get(self, get_endpoint, query_params=None):
|
||||
url = f"{self.base_url}/api/{get_endpoint}"
|
||||
if query_params:
|
||||
url = url + _query_params_to_string(query_params)
|
||||
|
||||
res = requests.get(url, headers=self.headers)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
def delete(self, endpoint):
|
||||
url = f"{self.base_url}/api/{endpoint}"
|
||||
try:
|
||||
# TODO: deletion works, but the request fails?
|
||||
res = requests.delete(url, headers=self.headers)
|
||||
res.raise_for_status()
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def put(self, endpoint, body):
|
||||
url = f"{self.base_url}/api/{endpoint}"
|
||||
res = requests.put(url, json=body, headers=self.headers)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
|
||||
def post(self, endpoint, body, query_params=None):
|
||||
url = f"{self.base_url}/api/{endpoint}" + _query_params_to_string(query_params)
|
||||
|
||||
res = requests.post(url, json=body, headers=self.headers)
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
# FIXME: Hack to make imports work with IDE. The ansible import path is not valid for a regular python
|
||||
# project.
|
||||
from plugins.module_utils.portainer import *
|
||||
except ImportError:
|
||||
from ansible_collections.chatton.portainer.plugins.module_utils.portainer import (
|
||||
PortainerClient,
|
||||
_query_params_to_string,
|
||||
)
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
---
|
||||
module: portainer_stack
|
||||
|
||||
short_description: This is my test module
|
||||
|
||||
# If this is part of a collection, you need to use semantic versioning,
|
||||
# i.e. the version is of the form "2.5.0" and not "2.4".
|
||||
version_added: "1.0.0"
|
||||
|
||||
description: This is my longer description explaining my test module.
|
||||
|
||||
options:
|
||||
name:
|
||||
description: This is the message to send to the test module.
|
||||
required: true
|
||||
type: str
|
||||
new:
|
||||
description:
|
||||
- Control to demo if the result of this module is changed or not.
|
||||
- Parameter description can be a list as well.
|
||||
required: false
|
||||
type: bool
|
||||
# Specify this value according to your collection
|
||||
# in format of namespace.collection.doc_fragment_name
|
||||
extends_documentation_fragment:
|
||||
- my_namespace.my_collection.my_doc_fragment_name
|
||||
|
||||
author:
|
||||
- Your Name (@chatton)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
# Deploy Gitea, Plex and Mealie stacks to portainer provided the files exist.
|
||||
- name: Portainer | Update Stack
|
||||
chatton.portainer.portainer_stack:
|
||||
username: admin
|
||||
password: "{{portainer.password}}"
|
||||
docker_compose_file_path: "/etc/docker-compose/{{ item.name }}/docker-compose.yml"
|
||||
stack_name: "{{ item.name }}"
|
||||
endpoint_id: "{{ item.endpoint_id }}"
|
||||
state: present
|
||||
with_items:
|
||||
- name: gitea
|
||||
endpoint_id: 1
|
||||
- name: plex
|
||||
endpoint_id: 2
|
||||
- name: mealie
|
||||
endpoint_id: 3
|
||||
|
||||
# Delete plex stack
|
||||
- name: Portainer | Delete Plex Stack
|
||||
chatton.portainer.portainer_stack:
|
||||
username: admin
|
||||
password: "{{portainer.password}}"
|
||||
stack_name: "plex"
|
||||
endpoint_id: "2"
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
# These are examples of possible return values, and in general should use other names for return values.
|
||||
username:
|
||||
description: The Portainer username.
|
||||
type: str
|
||||
returned: always
|
||||
sample: 'admin'
|
||||
password:
|
||||
description: The provided user's password.
|
||||
type: str
|
||||
returned: never
|
||||
sample: 'MyS00p3rS3cretPassw0rd'
|
||||
docker_compose_file_path:
|
||||
description: The path to a docker compose file which will be used to create the Portainer stack.
|
||||
type: str
|
||||
returned: never
|
||||
sample: ''
|
||||
"""
|
||||
|
||||
|
||||
COMPOSE_STACK = 2
|
||||
STRING_METHOD = "string"
|
||||
|
||||
|
||||
def _create_stack(client, module, file_contents):
|
||||
target_stack_name = module.params["stack_name"]
|
||||
body = {
|
||||
"name": target_stack_name,
|
||||
"stackFileContent": file_contents,
|
||||
}
|
||||
|
||||
query_params = {
|
||||
"type": COMPOSE_STACK,
|
||||
"method": STRING_METHOD,
|
||||
"endpointId": client.endpoint,
|
||||
}
|
||||
return client.post("stacks", body=body, query_params=query_params)
|
||||
|
||||
|
||||
def _update_stack(client, module, stack_id):
|
||||
target_stack_name = module.params["stack_name"]
|
||||
with open(module.params["docker_compose_file_path"]) as f:
|
||||
file_contents = f.read()
|
||||
return client.put(
|
||||
f"stacks/{stack_id}?&endpointId={client.endpoint}",
|
||||
body={
|
||||
"name": target_stack_name,
|
||||
"stackFileContent": file_contents,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def handle_state_present(client, module):
|
||||
result = dict(changed=False, stack_name=module.params["stack_name"])
|
||||
|
||||
already_exists = False
|
||||
stacks = client.get("stacks")
|
||||
result["stacks"] = stacks
|
||||
|
||||
with open(module.params["docker_compose_file_path"]) as f:
|
||||
file_contents = f.read()
|
||||
|
||||
target_stack_name = module.params["stack_name"]
|
||||
for stack in stacks:
|
||||
if stack["Name"] == target_stack_name:
|
||||
already_exists = True
|
||||
result["stack_id"] = stack["Id"]
|
||||
break
|
||||
|
||||
if not already_exists:
|
||||
stack = _create_stack(client, module, file_contents)
|
||||
result["changed"] = True
|
||||
result["stack_id"] = stack["Id"]
|
||||
module.exit_json(**result)
|
||||
return
|
||||
|
||||
stack_id = result["stack_id"]
|
||||
current_file_contents_resp = client.get(
|
||||
f"stacks/{stack_id}/file", query_params={"endpointId": client.endpoint}
|
||||
)
|
||||
|
||||
result["are_equal"] = (
|
||||
current_file_contents_resp["StackFileContent"] == file_contents
|
||||
)
|
||||
if result["are_equal"]:
|
||||
module.exit_json(**result)
|
||||
return
|
||||
|
||||
# the stack exists and we have a new config.
|
||||
_update_stack(client, module, stack_id)
|
||||
result["changed"] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
def handle_state_absent(client, module):
|
||||
result = dict(changed=False, stack_name=module.params["stack_name"])
|
||||
already_exists = False
|
||||
target_stack_name = module.params["stack_name"]
|
||||
stacks = client.get("stacks")
|
||||
for stack in stacks:
|
||||
if stack["Name"] == target_stack_name:
|
||||
already_exists = True
|
||||
result["stack_id"] = stack["Id"]
|
||||
break
|
||||
|
||||
if not already_exists:
|
||||
module.exit_json(**result)
|
||||
return
|
||||
|
||||
stack_id = result["stack_id"]
|
||||
client.delete(
|
||||
f"stacks/{stack_id}" + _query_params_to_string({"endpointId": client.endpoint})
|
||||
)
|
||||
result["changed"] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
def run_module():
|
||||
# define available arguments/parameters a user can pass to the module
|
||||
module_args = dict(
|
||||
stack_name=dict(type="str", required=True),
|
||||
docker_compose_file_path=dict(type="str"),
|
||||
username=dict(type="str", default="admin"),
|
||||
password=dict(type="str", required=True, no_log=True),
|
||||
endpoint_id=dict(type="int", required=True),
|
||||
base_url=dict(type="str", default="http://localhost:9000"),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
# docker compose file is only required if we are ensuring the stack is present.
|
||||
["state", "present", ("docker_compose_file_path",)],
|
||||
]
|
||||
|
||||
state_fns = {"present": handle_state_present, "absent": handle_state_absent}
|
||||
|
||||
# the AnsibleModule object will be our abstraction working with Ansible
|
||||
# this includes instantiation, a couple of common attr would be the
|
||||
# args/params passed to the execution, as well as if the module
|
||||
# supports check mode
|
||||
module = AnsibleModule(
|
||||
argument_spec=module_args,
|
||||
required_if=required_if,
|
||||
# TODO: support check mode
|
||||
supports_check_mode=False,
|
||||
)
|
||||
|
||||
client = PortainerClient(
|
||||
base_url=module.params["base_url"], endpoint=module.params["endpoint_id"]
|
||||
)
|
||||
client.login(module.params["username"], module.params["password"])
|
||||
|
||||
state_fns[module.params["state"]](client, module)
|
||||
|
||||
|
||||
def main():
|
||||
run_module()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,19 @@
|
||||
ansible==6.3.0
|
||||
ansible-core==2.13.3
|
||||
certifi==2022.6.15
|
||||
cffi==1.15.1
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
colorama==0.4.5
|
||||
cryptography==37.0.4
|
||||
idna==3.3
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.1
|
||||
packaging==21.3
|
||||
portainer-py==0.7.6
|
||||
pycparser==2.21
|
||||
pyparsing==3.0.9
|
||||
PyYAML==6.0
|
||||
requests==2.28.1
|
||||
resolvelib==0.8.1
|
||||
urllib3==1.26.12
|
||||
@ -0,0 +1,9 @@
|
||||
import unittest
|
||||
|
||||
|
||||
class TestMyModule(unittest.TestCase):
|
||||
def test_foo(self):
|
||||
assert True
|
||||
|
||||
def test_foo2(self):
|
||||
assert False
|
||||
@ -0,0 +1,2 @@
|
||||
[flake8]
|
||||
exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg,.*env
|
||||
@ -0,0 +1,68 @@
|
||||
---
|
||||
name: CI
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
schedule:
|
||||
- cron: "0 5 * * 1"
|
||||
|
||||
jobs:
|
||||
# test the role
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- image: geerlingguy/docker-centos8-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-centos7-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-fedora32-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-fedora31-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-fedora30-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-ubuntu2004-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-ubuntu2004-ansible
|
||||
mode: package_manager
|
||||
- image: geerlingguy/docker-ubuntu1804-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-ubuntu1604-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-debian10-ansible
|
||||
mode: package_manager
|
||||
- image: geerlingguy/docker-debian10-ansible
|
||||
mode: github_releases
|
||||
- image: geerlingguy/docker-debian9-ansible
|
||||
mode: github_releases
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python 3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.in
|
||||
|
||||
- name: Run molecule tests
|
||||
env:
|
||||
IMAGE: ${{ matrix.config.image }}
|
||||
INSTALL_MODE: ${{ matrix.config.mode }}
|
||||
run: molecule -v test
|
||||
|
||||
# publish the role on ansible galaxy
|
||||
publish:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Publish
|
||||
uses: robertdebock/galaxy-action@1.1.0
|
||||
with:
|
||||
galaxy_api_key: ${{ secrets.GALAXY_API_KEY }}
|
||||
@ -0,0 +1,5 @@
|
||||
*.retry
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*env/
|
||||
.cache/
|
||||
@ -0,0 +1,9 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
ignore: |
|
||||
.*env/
|
||||
|
||||
rules:
|
||||
line-length:
|
||||
max: 120
|
||||
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Sylvain Prat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -0,0 +1,54 @@
|
||||
Ansible Role: mergerfs
|
||||
======================
|
||||
|
||||
[![Build Status][build_badge]][build_link]
|
||||
[![Ansible Galaxy][galaxy_badge]][galaxy_link]
|
||||
|
||||
Install and configure Mergerfs — A featureful union filesystem.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
None.
|
||||
|
||||
Role Variables
|
||||
--------------
|
||||
|
||||
See [defaults/main.yml](defaults/main.yml).
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
None.
|
||||
|
||||
Example Playbook
|
||||
----------------
|
||||
|
||||
```yaml
|
||||
- hosts: server
|
||||
roles:
|
||||
- role: sprat.mergerfs
|
||||
vars:
|
||||
mergerfs_mounts:
|
||||
- path: /mnt/data
|
||||
branches:
|
||||
- /mnt/data1
|
||||
- /mnt/data2
|
||||
options: allow_other,use_ino
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT
|
||||
|
||||
Author Information
|
||||
------------------
|
||||
|
||||
This role was created in 2020 by [Sylvain Prat](https://github.com/sprat).
|
||||
|
||||
|
||||
[build_badge]: https://img.shields.io/github/workflow/status/sprat/ansible-role-mergerfs/CI
|
||||
[build_link]: https://github.com/sprat/ansible-role-mergerfs/actions?query=workflow:CI
|
||||
[galaxy_badge]: https://img.shields.io/ansible/role/47517
|
||||
[galaxy_link]: https://galaxy.ansible.com/sprat/mergerfs
|
||||
@ -0,0 +1,23 @@
|
||||
---
|
||||
# Install mode: defines where to download and install the package from:
|
||||
# - "github_releases": install from Mergerfs' GitHub releases
|
||||
# - "package_manager": install from the Linux distribution package manager.
|
||||
# Note that the mergerfs package does not exists in all distributions, so it
|
||||
# may not work for you.
|
||||
mergerfs_install_mode: github_releases
|
||||
|
||||
# Version to install: "latest" version or a specific version number, e.g. "2.28.2"
|
||||
# This setting only applies in "github_releases" mode
|
||||
mergerfs_version: latest
|
||||
|
||||
# Mergerfs mountpoints to create. For example:
|
||||
# mergerfs_mounts:
|
||||
# - path: /mnt/storage
|
||||
# branches:
|
||||
# - /mnt/data*
|
||||
# - /mnt/other
|
||||
# options: allow_other,use_ino
|
||||
mergerfs_mounts: []
|
||||
|
||||
# Url of the mergerfs GitHub releases page
|
||||
mergerfs_github_releases_url: https://github.com/trapexit/mergerfs/releases
|
||||
@ -0,0 +1,2 @@
|
||||
install_date: Fri 2 Sep 21:20:46 2022
|
||||
version: master
|
||||
@ -0,0 +1,30 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: Sylvain Prat
|
||||
role_name: mergerfs
|
||||
namespace: sprat
|
||||
description: Install and configure Mergerfs — A featureful union filesystem
|
||||
license: MIT
|
||||
company: none
|
||||
min_ansible_version: 2.3
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- all
|
||||
- name: Debian
|
||||
versions:
|
||||
- all
|
||||
- name: Fedora
|
||||
versions:
|
||||
- all
|
||||
- name: EL
|
||||
versions:
|
||||
- all
|
||||
galaxy_tags:
|
||||
- mergerfs
|
||||
- union
|
||||
- filesystem
|
||||
- disk
|
||||
- mount
|
||||
|
||||
dependencies: []
|
||||
@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
vars:
|
||||
mergerfs_mounts:
|
||||
- path: /mnt/storage
|
||||
branches:
|
||||
- /mnt/data*
|
||||
options: allow_other,use_ino
|
||||
roles:
|
||||
- role: ansible-role-mergerfs
|
||||
@ -0,0 +1,21 @@
|
||||
---
|
||||
dependency:
|
||||
name: galaxy
|
||||
driver:
|
||||
name: docker
|
||||
lint: yamllint -s . && ansible-lint . && flake8
|
||||
platforms:
|
||||
- name: instance
|
||||
image: ${IMAGE:-geerlingguy/docker-ubuntu2004-ansible}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
provisioner:
|
||||
name: ansible
|
||||
inventory:
|
||||
group_vars:
|
||||
all:
|
||||
mergerfs_install_mode: ${INSTALL_MODE:-github_releases}
|
||||
verifier:
|
||||
name: testinfra
|
||||
@ -0,0 +1,25 @@
|
||||
---
|
||||
- name: Prepare
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: Create directories
|
||||
become: true
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
loop:
|
||||
- /mnt/data1
|
||||
- /mnt/data2
|
||||
|
||||
- name: Create data files
|
||||
become: true
|
||||
copy:
|
||||
content: "{{ item.content }}\n"
|
||||
dest: "{{ item.path }}"
|
||||
loop:
|
||||
- path: /mnt/data1/file1.txt
|
||||
content: file1
|
||||
- path: /mnt/data2/file2.txt
|
||||
content: file2
|
||||
- path: /mnt/data2/file3.txt
|
||||
content: file3
|
||||
@ -0,0 +1,21 @@
|
||||
import os
|
||||
|
||||
import testinfra.utils.ansible_runner
|
||||
|
||||
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
|
||||
os.environ['MOLECULE_INVENTORY_FILE']
|
||||
).get_hosts('all')
|
||||
|
||||
|
||||
def test_mount_point(host):
|
||||
mount_point = host.mount_point('/mnt/storage')
|
||||
assert mount_point.exists
|
||||
assert mount_point.filesystem == 'fuse.mergerfs'
|
||||
assert 'allow_other' in mount_point.options
|
||||
# assert 'use_ino' in mount_point.options
|
||||
|
||||
|
||||
def test_data_files(host):
|
||||
assert host.file('/mnt/storage/file1.txt').exists
|
||||
assert host.file('/mnt/storage/file2.txt').exists
|
||||
assert host.file('/mnt/storage/file3.txt').exists
|
||||
@ -0,0 +1 @@
|
||||
molecule[ansible,docker,test,lint]
|
||||
@ -0,0 +1,249 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile
|
||||
#
|
||||
ansi2html==1.6.0
|
||||
# via molecule
|
||||
ansible-base==2.10.7
|
||||
# via ansible
|
||||
ansible-lint==5.0.7
|
||||
# via molecule
|
||||
ansible==3.2.0
|
||||
# via molecule
|
||||
apipkg==1.5
|
||||
# via execnet
|
||||
appdirs==1.4.4
|
||||
# via virtualenv
|
||||
arrow==1.0.3
|
||||
# via jinja2-time
|
||||
attrs==20.3.0
|
||||
# via pytest
|
||||
bcrypt==3.2.0
|
||||
# via paramiko
|
||||
binaryornot==0.4.4
|
||||
# via cookiecutter
|
||||
bracex==2.1.1
|
||||
# via wcmatch
|
||||
cerberus==1.3.2
|
||||
# via molecule
|
||||
certifi==2020.12.5
|
||||
# via requests
|
||||
cffi==1.14.5
|
||||
# via
|
||||
# bcrypt
|
||||
# cryptography
|
||||
# pynacl
|
||||
cfgv==3.2.0
|
||||
# via pre-commit
|
||||
chardet==4.0.0
|
||||
# via
|
||||
# binaryornot
|
||||
# requests
|
||||
click-completion==0.5.2
|
||||
# via molecule
|
||||
click-help-colors==0.9
|
||||
# via molecule
|
||||
click==7.1.2
|
||||
# via
|
||||
# click-completion
|
||||
# click-help-colors
|
||||
# cookiecutter
|
||||
# molecule
|
||||
colorama==0.4.4
|
||||
# via rich
|
||||
commonmark==0.9.1
|
||||
# via rich
|
||||
cookiecutter==1.7.2
|
||||
# via molecule
|
||||
coverage==5.5
|
||||
# via pytest-cov
|
||||
cryptography==3.4.7
|
||||
# via
|
||||
# ansible-base
|
||||
# paramiko
|
||||
distlib==0.3.1
|
||||
# via virtualenv
|
||||
distro==1.5.0
|
||||
# via selinux
|
||||
docker==5.0.0
|
||||
# via molecule-docker
|
||||
enrich==1.2.6
|
||||
# via
|
||||
# ansible-lint
|
||||
# molecule
|
||||
execnet==1.8.0
|
||||
# via pytest-xdist
|
||||
filelock==3.0.12
|
||||
# via virtualenv
|
||||
flake8==3.9.0
|
||||
# via molecule
|
||||
identify==2.2.3
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
# via requests
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
jinja2-time==0.2.0
|
||||
# via cookiecutter
|
||||
jinja2==2.11.3
|
||||
# via
|
||||
# ansible-base
|
||||
# click-completion
|
||||
# cookiecutter
|
||||
# jinja2-time
|
||||
# molecule
|
||||
markupsafe==1.1.1
|
||||
# via
|
||||
# cookiecutter
|
||||
# jinja2
|
||||
mccabe==0.6.1
|
||||
# via flake8
|
||||
molecule-docker==0.2.4
|
||||
# via molecule
|
||||
molecule[ansible,docker,lint,test]==3.3.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# molecule-docker
|
||||
more-itertools==8.7.0
|
||||
# via pytest-plus
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
packaging==20.9
|
||||
# via
|
||||
# ansible-base
|
||||
# ansible-lint
|
||||
# molecule
|
||||
# pytest
|
||||
paramiko==2.7.2
|
||||
# via molecule
|
||||
pathspec==0.8.1
|
||||
# via yamllint
|
||||
pexpect==4.8.0
|
||||
# via molecule
|
||||
pluggy==0.13.1
|
||||
# via
|
||||
# molecule
|
||||
# pytest
|
||||
poyo==0.5.0
|
||||
# via cookiecutter
|
||||
pre-commit==2.12.0
|
||||
# via molecule
|
||||
ptyprocess==0.7.0
|
||||
# via pexpect
|
||||
py==1.10.0
|
||||
# via
|
||||
# pytest
|
||||
# pytest-forked
|
||||
pycodestyle==2.7.0
|
||||
# via flake8
|
||||
pycparser==2.20
|
||||
# via cffi
|
||||
pyflakes==2.3.1
|
||||
# via flake8
|
||||
pygments==2.8.1
|
||||
# via rich
|
||||
pynacl==1.4.0
|
||||
# via paramiko
|
||||
pyparsing==2.4.7
|
||||
# via packaging
|
||||
pytest-cov==2.11.1
|
||||
# via molecule
|
||||
pytest-forked==1.3.0
|
||||
# via pytest-xdist
|
||||
pytest-helpers-namespace==2021.3.24
|
||||
# via molecule
|
||||
pytest-html==3.1.1
|
||||
# via molecule
|
||||
pytest-metadata==1.11.0
|
||||
# via pytest-html
|
||||
pytest-mock==3.5.1
|
||||
# via molecule
|
||||
pytest-plus==0.2
|
||||
# via molecule
|
||||
pytest-testinfra==6.2.0
|
||||
# via molecule
|
||||
pytest-verbose-parametrize==1.7.0
|
||||
# via molecule
|
||||
pytest-xdist==2.2.1
|
||||
# via molecule
|
||||
pytest==6.2.3
|
||||
# via
|
||||
# molecule
|
||||
# pytest-cov
|
||||
# pytest-forked
|
||||
# pytest-helpers-namespace
|
||||
# pytest-html
|
||||
# pytest-metadata
|
||||
# pytest-mock
|
||||
# pytest-plus
|
||||
# pytest-testinfra
|
||||
# pytest-verbose-parametrize
|
||||
# pytest-xdist
|
||||
python-dateutil==2.8.1
|
||||
# via arrow
|
||||
python-slugify==4.0.1
|
||||
# via cookiecutter
|
||||
pyyaml==5.4.1
|
||||
# via
|
||||
# ansible-base
|
||||
# ansible-lint
|
||||
# molecule
|
||||
# pre-commit
|
||||
# yamllint
|
||||
requests==2.25.1
|
||||
# via
|
||||
# cookiecutter
|
||||
# docker
|
||||
rich==10.1.0
|
||||
# via
|
||||
# ansible-lint
|
||||
# enrich
|
||||
# molecule
|
||||
ruamel.yaml.clib==0.2.2
|
||||
# via ruamel.yaml
|
||||
ruamel.yaml==0.17.4
|
||||
# via ansible-lint
|
||||
selinux==0.2.1
|
||||
# via
|
||||
# molecule
|
||||
# molecule-docker
|
||||
shellingham==1.4.0
|
||||
# via click-completion
|
||||
six==1.15.0
|
||||
# via
|
||||
# bcrypt
|
||||
# click-completion
|
||||
# cookiecutter
|
||||
# pynacl
|
||||
# pytest-verbose-parametrize
|
||||
# python-dateutil
|
||||
# tenacity
|
||||
# virtualenv
|
||||
# websocket-client
|
||||
subprocess-tee==0.2.0
|
||||
# via molecule
|
||||
tenacity==7.0.0
|
||||
# via ansible-lint
|
||||
text-unidecode==1.3
|
||||
# via python-slugify
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pre-commit
|
||||
# pytest
|
||||
typing-extensions==3.7.4.3
|
||||
# via rich
|
||||
urllib3==1.26.4
|
||||
# via requests
|
||||
virtualenv==20.4.3
|
||||
# via pre-commit
|
||||
wcmatch==8.1.2
|
||||
# via ansible-lint
|
||||
websocket-client==0.58.0
|
||||
# via docker
|
||||
yamllint==1.26.1
|
||||
# via molecule
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
@ -0,0 +1,54 @@
|
||||
---
|
||||
# Note: we don't use the GitHub API to retrieve the latest version because
|
||||
# it has rate limits which are hard to avoid in CI (we need a token, authenticate
|
||||
# with the API, etc.). Instead, we browse the latest release url which redirects
|
||||
# to the release page, where we can find the version number in the URL.
|
||||
- become: false
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
block:
|
||||
- name: Get latest release information from GitHub
|
||||
uri:
|
||||
url: "{{ mergerfs_github_releases_url }}/latest"
|
||||
register: mergerfs_github_release_page
|
||||
- name: Set latest mergerfs version fact
|
||||
set_fact:
|
||||
mergerfs_version: "{{ mergerfs_github_release_page['url'].split('/')[-1] }}"
|
||||
when: mergerfs_version == "latest"
|
||||
|
||||
- name: Determine package download url
|
||||
set_fact:
|
||||
mergerfs_package_url: "{{ mergerfs_github_releases_url }}/download/{{ mergerfs_version }}/\
|
||||
{{ mergerfs_pkg_prefix }}{{ mergerfs_version }}{{ mergerfs_pkg_suffix }}"
|
||||
|
||||
- name: Install xz-utils package for .deb package installation
|
||||
become: true
|
||||
apt:
|
||||
name: xz-utils
|
||||
state: present
|
||||
update_cache: true
|
||||
when: ansible_pkg_mgr == 'apt'
|
||||
|
||||
- name: Install mergerfs package with apt
|
||||
become: true
|
||||
apt:
|
||||
deb: "{{ mergerfs_package_url }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
when: ansible_pkg_mgr == 'apt'
|
||||
|
||||
- name: Install mergerfs package with yum
|
||||
become: true
|
||||
yum:
|
||||
name: "{{ mergerfs_package_url }}"
|
||||
state: present
|
||||
disable_gpg_check: true # the package is not signed
|
||||
when: ansible_pkg_mgr == 'yum'
|
||||
|
||||
- name: Install mergerfs package with dnf
|
||||
become: true
|
||||
dnf:
|
||||
name: "{{ mergerfs_package_url }}"
|
||||
state: present
|
||||
disable_gpg_check: true # the package is not signed
|
||||
when: ansible_pkg_mgr == 'dnf'
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Install mergerfs package with package manager
|
||||
become: true
|
||||
package:
|
||||
name: mergerfs
|
||||
state: present
|
||||
update_cache: true
|
||||
@ -0,0 +1,34 @@
|
||||
---
|
||||
- name: Include OS-specific variables
|
||||
include_vars: "{{ ansible_os_family }}.yml"
|
||||
tags:
|
||||
- mergerfs
|
||||
|
||||
- name: Install mergerfs prerequisites
|
||||
become: true
|
||||
package:
|
||||
name: "{{ mergerfs_prerequisites }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
tags:
|
||||
- mergerfs
|
||||
- mergerfs_install
|
||||
|
||||
- name: Include install tasks
|
||||
import_tasks: install_from_{{ mergerfs_install_mode }}.yml
|
||||
tags:
|
||||
- mergerfs
|
||||
- mergerfs_install
|
||||
|
||||
- name: Mount mergerfs filesystems
|
||||
become: true
|
||||
mount:
|
||||
fstype: fuse.mergerfs
|
||||
src: "{{ ':'.join(item.branches | mandatory) }}"
|
||||
path: "{{ item.path | mandatory }}"
|
||||
opts: "{{ item.options | default('defaults') }}"
|
||||
state: "{{ item.state | default('mounted') }}"
|
||||
loop: "{{ mergerfs_mounts }}"
|
||||
tags:
|
||||
- mergerfs
|
||||
- mergerfs_mount
|
||||
@ -0,0 +1,12 @@
|
||||
---
|
||||
mergerfs_prerequisites:
|
||||
- fuse
|
||||
mergerfs_dist: "{{ ansible_distribution|lower }}-{{ ansible_distribution_release }}"
|
||||
mergerfs_arch_map:
|
||||
x86_64: amd64
|
||||
i386: i386
|
||||
aarch64: arm64
|
||||
armv7l: armhf
|
||||
mergerfs_arch: "{{ mergerfs_arch_map[ansible_userspace_architecture | default(ansible_architecture) ] }}"
|
||||
mergerfs_pkg_prefix: "mergerfs_"
|
||||
mergerfs_pkg_suffix: ".{{ mergerfs_dist }}_{{ mergerfs_arch }}.deb"
|
||||
@ -0,0 +1,7 @@
|
||||
---
|
||||
mergerfs_prerequisites:
|
||||
- fuse
|
||||
mergerfs_dist: "{{ 'fc' if ansible_distribution == 'Fedora' else 'el' }}{{ ansible_distribution_major_version }}"
|
||||
mergerfs_arch: "{{ ansible_userspace_architecture }}"
|
||||
mergerfs_pkg_prefix: "mergerfs-"
|
||||
mergerfs_pkg_suffix: "-1.{{ mergerfs_dist }}.{{ mergerfs_arch }}.rpm"
|
||||
Loading…
Reference in New Issue