diff --git a/bootstrap.yml b/bootstrap.yml index 4389a42..4905df8 100644 --- a/bootstrap.yml +++ b/bootstrap.yml @@ -1,27 +1,5 @@ --- - hosts: all become: true - tasks: - - name: Update Packages - apt: - upgrade: dist - update_cache: true - - - name: Create ansible user - user: - name: ansible - groups: root - - - name: Add ssh key for ansible - authorized_key: - user: "ansible" - state: present - key: "{{ lookup('file', '~/.ssh/ansible.pub') }}" - - - name: Add sudoers file for ansible - copy: - src: sudoer_ansible - dest: /etc/sudoers.d/ansible - owner: root - group: root - mode: 0440 + roles: + - role: 'roles/bootstrap' diff --git a/group_vars/linode.yml b/group_vars/linode.yml index be21341..81839d9 100644 --- a/group_vars/linode.yml +++ b/group_vars/linode.yml @@ -2,17 +2,14 @@ # all encrypted variables should go in the linked file. vault_file: vault_vars/linode-vault.yml # any linode specific variables go here -configure_mergefs: false services: - name: gitea - name: mealie - name: linkding - name: overseerr - name: nextcloud -# - name: dashboards - name: nginx-proxy-manager - name: uptime-kuma -# - name: vpn-stack - name: docker-volume-backup - name: mariadb - name: photoprism @@ -22,7 +19,7 @@ services: docker_networks: - mariadb_net -# use raw docker compose instead of portainer -use_docker_compose: true -use_portainer: false +# use docker compose +container_deployment_mode: "compose" + restore_from_s3: false diff --git a/group_vars/qnap.yml b/group_vars/qnap.yml index 03ee1bb..e0f4268 100644 --- a/group_vars/qnap.yml +++ b/group_vars/qnap.yml @@ -2,7 +2,6 @@ # all encrypted variables should go in the linked file. vault_file: vault_vars/qnap-vault.yml # any qnap specific variables go here -configure_mergefs: true mounts: - path: /mnt/mergerfs state: mounted @@ -25,24 +24,38 @@ devices: services: - name: gitea + endpoint_id: 2 - name: mealie + endpoint_id: 2 - name: linkding + endpoint_id: 2 - name: overseerr + endpoint_id: 2 - name: nextcloud + endpoint_id: 2 - name: dashboards + endpoint_id: 2 - name: nginx-proxy-manager + endpoint_id: 2 - name: plex + endpoint_id: 2 - name: uptime-kuma + endpoint_id: 2 - name: vpn-stack + endpoint_id: 2 - name: docker-volume-backup + endpoint_id: 2 - name: mariadb + endpoint_id: 2 - name: photoprism + endpoint_id: 2 - name: olivetin + endpoint_id: 2 # any additional docker networks that should be created docker_networks: - mariadb_net -use_portainer: true -use_docker_compose: false +# use portainer +container_deployment_mode: "portainer" restore_from_s3: true diff --git a/group_vars/servers.yml b/group_vars/servers.yml index 9c25acb..38f9a90 100644 --- a/group_vars/servers.yml +++ b/group_vars/servers.yml @@ -39,4 +39,3 @@ shares: - /share/public_files - /share/private_files - /share/cian_files - diff --git a/hosts.ini b/hosts.ini index 7193ce6..ad087ed 100644 --- a/hosts.ini +++ b/hosts.ini @@ -5,7 +5,7 @@ qnap linode [qnap] -cianhatton@qnap +qnap # BEGIN ANSIBLE MANAGED BLOCK [linode] diff --git a/library/portainer.py b/library/portainer.py deleted file mode 100755 index 5993ea8..0000000 --- a/library/portainer.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/python - -from __future__ import (absolute_import, division, print_function) - -__metaclass__ = type - -import requests - -DOCUMENTATION = r''' ---- -module: my_test - -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''' -# Pass in a message -- name: Test with a message - my_namespace.my_collection.my_test: - name: hello world - -# pass in a message and have changed true -- name: Test with a message and changed output - my_namespace.my_collection.my_test: - name: hello world - new: true - -# fail the module -- name: Test failure of the module - my_namespace.my_collection.my_test: - name: fail me -''' - -RETURN = r''' -# These are examples of possible return values, and in general should use other names for return values. -original_message: - description: The original name param that was passed in. - type: str - returned: always - sample: 'hello world' -message: - description: The output message that the test module generates. - type: str - returned: always - sample: 'goodbye' -''' - -from ansible.module_utils.basic import AnsibleModule - - -def _extract_creds(module): - return { - "username": module.params["username"], - "password": module.params["password"], - "base_url": module.params["base_url"], - } - - -def _get_jwt_token(creds): - payload = { - "Username": creds["username"], - "Password": creds["password"], - } - - base_url = creds["base_url"] - auth_url = f"{base_url}/api/auth" - resp = requests.post(auth_url, json=payload) - resp.raise_for_status() - return resp.json()["jwt"] - - -COMPOSE_STACK = 2 -STRING_METHOD = "string" - - -def _query_params_to_string(params): - s = "?" - for k, v in params.items(): - s += f"&{k}={v}" - return s - - -def _load_envs_from_file(filepath): - envs = [] - with open(filepath) as f: - file_contents = f.read() - lines = file_contents.splitlines() - for line in lines: - name, value = line.split("=") - envs.append({ - "name": name, - "value": value - }) - return envs - - -class PortainerClient: - def __init__(self, creds): - self.base_url = creds["base_url"] - self.token = _get_jwt_token(creds) - self.headers = { - "Authorization": f"Bearer {self.token}" - } - - def get(self, endpoint, query_params=None): - url = f"{self.base_url}/api/{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() - - -def _create_stack(client, module, file_contents, envs=None): - if not envs: - envs = [] - target_stack_name = module.params["stack_name"] - body = { - "env": envs, - "name": target_stack_name, - "stackFileContent": file_contents, - } - - query_params = { - "type": COMPOSE_STACK, - "method": STRING_METHOD, - "endpointId": 2, - } - return client.post("stacks", body=body, query_params=query_params) - - -def _update_stack(client, module, stack_id, envs=None): - if not envs: - envs = [] - 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=2", body={ - "env": envs, - "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": 2 - }) - - 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": 2})) - 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', required=True), - username=dict(type='str', default='admin'), - password=dict(type='str', required=True, no_log=True), - base_url=dict(type='str', default="http://localhost:9000"), - state=dict(type='str', default="present", choices=['present', 'absent']) - ) - - 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, - supports_check_mode=False - ) - - client = PortainerClient(creds=_extract_creds(module)) - state_fns[module.params["state"]](client, module) - - -def main(): - run_module() - - -if __name__ == '__main__': - main() diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..fdebb8e --- /dev/null +++ b/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: https://github.com/chatton/ansible-portainer.git + type: git + version: master diff --git a/files/sudoer_ansible b/roles/bootstrap/files/sudoer_ansible similarity index 100% rename from files/sudoer_ansible rename to roles/bootstrap/files/sudoer_ansible diff --git a/roles/bootstrap/tasks/main.yml b/roles/bootstrap/tasks/main.yml new file mode 100644 index 0000000..8ffcfac --- /dev/null +++ b/roles/bootstrap/tasks/main.yml @@ -0,0 +1,24 @@ +--- +- name: Update Packages + apt: + upgrade: dist + update_cache: true + +- name: Create ansible user + user: + name: ansible + groups: root + +- name: Add ssh key for ansible + authorized_key: + user: "ansible" + state: present + key: "{{ lookup('file', '~/.ssh/ansible.pub') }}" + +- name: Add sudoers file for ansible + copy: + src: sudoer_ansible + dest: /etc/sudoers.d/ansible + owner: root + group: root + mode: 0440 diff --git a/roles/setup_hosted_services/defaults/main.yml b/roles/setup_hosted_services/defaults/main.yml index 300cf0a..8c347f3 100644 --- a/roles/setup_hosted_services/defaults/main.yml +++ b/roles/setup_hosted_services/defaults/main.yml @@ -14,6 +14,3 @@ qnap: backups_dir: /mnt/mergerfs/backups # path where photoprism photos are stored photoprism_dir: /mnt/mergerfs/photoprism - -use_portainer: true -use_docker_compose: false diff --git a/roles/setup_hosted_services/meta/main.yml b/roles/setup_hosted_services/meta/main.yml index fe3deea..ad73214 100644 --- a/roles/setup_hosted_services/meta/main.yml +++ b/roles/setup_hosted_services/meta/main.yml @@ -1,45 +1,10 @@ galaxy_info: - author: your name + author: Cian Hatton namespace: chatton description: your role description company: your company (optional) - - # If the issue tracker for your role is not on github, uncomment the - # next line and provide a value - # issue_tracker_url: http://example.com/issue/tracker - - # Choose a valid license ID from https://spdx.org - some suggested licenses: - # - BSD-3-Clause (default) - # - MIT - # - GPL-2.0-or-later - # - GPL-3.0-only - # - Apache-2.0 - # - CC-BY-4.0 license: license (GPL-2.0-or-later, MIT, etc) - min_ansible_version: 2.1 - - # If this a Container Enabled role, provide the minimum Ansible Container version. - # min_ansible_container_version: - - # - # Provide a list of supported platforms, and for each platform a list of versions. - # If you don't wish to enumerate all versions for a particular platform, use 'all'. - # To view available platforms and versions (or releases), visit: - # https://galaxy.ansible.com/api/v1/platforms/ - # - # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 - galaxy_tags: [] # List tags for your role here, one per line. A tag is a keyword that describes # and categorizes the role. Users find roles by searching for tags. Be sure to diff --git a/roles/setup_hosted_services/tasks/main.yml b/roles/setup_hosted_services/tasks/main.yml index 380dcc4..2fbd699 100644 --- a/roles/setup_hosted_services/tasks/main.yml +++ b/roles/setup_hosted_services/tasks/main.yml @@ -103,16 +103,18 @@ with_items: "{{ docker_networks }}" - name: Portainer | Update Stack - when: use_portainer - portainer: + when: container_deployment_mode == "portainer" + chatton.portainer.portainer_stack: username: admin password: "{{portainer.password}}" docker_compose_file_path: "{{qnap.docker_compose_directory}}/{{ item.name }}/docker-compose.yml" stack_name: "{{ item.name }}" + endpoint_id: "{{ item.endpoint_id }}" + state: present with_items: "{{services}}" - name: Docker compose | Update Stack - when: use_docker_compose + when: container_deployment_mode == "compose" docker_compose: project_src: "{{qnap.docker_compose_directory}}/{{ item.name }}" state: present diff --git a/setup-homelab.yml b/setup-homelab.yml index 3d4d391..e8e6b6c 100644 --- a/setup-homelab.yml +++ b/setup-homelab.yml @@ -1,5 +1,5 @@ --- -- hosts: servers +- hosts: qnap become: true pre_tasks: @@ -15,16 +15,36 @@ roles: - role: 'roles/setup_mergerfs' tags: ["mergerfs"] - when: configure_mergefs - role: 'roles/setup_users' tags: ["users"] - role: 'roles/setup_samba' tags: ["samba"] - when: configure_samba - role: 'roles/setup_docker' tags: ["docker"] - role: 'roles/setup_portainer' tags: ["portainer"] - when: use_portainer + - role: 'roles/setup_hosted_services' + tags: ["services"] + +- hosts: linode + become: true + + pre_tasks: + - name: Include vault variables. + include_vars: "{{vault_file}}" + tags: ["always"] + - name: Update Packages + apt: + upgrade: dist + update_cache: true + tags: ["always"] + + roles: + - role: 'roles/setup_users' + tags: ["users"] + - role: 'roles/setup_samba' + tags: ["samba"] + - role: 'roles/setup_docker' + tags: ["docker"] - role: 'roles/setup_hosted_services' tags: ["services"]