From 32ff55a15ae30586716a4b70be8363c02c425661 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 7 Oct 2019 13:01:42 +0200 Subject: [PATCH] Separate ingress IP draft --- config.cfg | 4 + library/digital_ocean_floating_ip.py | 335 ++++++++++++++++++++++++ playbooks/cloud-post.yml | 12 +- playbooks/tmpfs/linux.yml | 2 +- playbooks/tmpfs/macos.yml | 2 +- roles/cloud-digitalocean/tasks/main.yml | 9 + server.yml | 3 +- 7 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 library/digital_ocean_floating_ip.py diff --git a/config.cfg b/config.cfg index 5cb3eaa..380f78e 100644 --- a/config.cfg +++ b/config.cfg @@ -43,6 +43,10 @@ wireguard_PersistentKeepalive: 0 wireguard_network_ipv4: 10.19.49.0/24 wireguard_network_ipv6: fd9d:bc11:4021::/48 +# Used to segregate the incoming traffic to a separate IP +# Available for the following cloud providers: DigitalOcean +static_ip: true + # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission # Unit) than the normal value of 1500 and if you don't reduce the MTU of your diff --git a/library/digital_ocean_floating_ip.py b/library/digital_ocean_floating_ip.py new file mode 100644 index 0000000..375921b --- /dev/null +++ b/library/digital_ocean_floating_ip.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Patrick F. Marques +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: digital_ocean_floating_ip +short_description: Manage DigitalOcean Floating IPs +description: + - Create/delete/assign a floating IP. +version_added: "2.4" +author: "Patrick Marques (@pmarques)" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent'] + ip: + description: + - Public IP address of the Floating IP. Used to remove an IP + region: + description: + - The region that the Floating IP is reserved to. + droplet_id: + description: + - The Droplet that the Floating IP has been assigned to. + oauth_token: + description: + - DigitalOcean OAuth token. + required: true +notes: + - Version 2 of DigitalOcean API is used. +requirements: + - "python >= 2.6" +''' + + +EXAMPLES = ''' +- name: "Create a Floating IP in region lon1" + digital_ocean_floating_ip: + state: present + region: lon1 + +- name: "Create a Floating IP assigned to Droplet ID 123456" + digital_ocean_floating_ip: + state: present + droplet_id: 123456 + +- name: "Delete a Floating IP with ip 1.2.3.4" + digital_ocean_floating_ip: + state: absent + ip: "1.2.3.4" + +''' + + +RETURN = ''' +# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#floating-ips +data: + description: a DigitalOcean Floating IP resource + returned: success and no resource constraint + type: dict + sample: { + "action": { + "id": 68212728, + "status": "in-progress", + "type": "assign_ip", + "started_at": "2015-10-15T17:45:44Z", + "completed_at": null, + "resource_id": 758603823, + "resource_type": "floating_ip", + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + "512mb", + "1gb", + "2gb", + "4gb", + "8gb", + "16gb", + "32gb", + "48gb", + "64gb" + ], + "features": [ + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": true + }, + "region_slug": "nyc3" + } + } +''' + +import json +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.urls import fetch_url + + +class Response(object): + + def __init__(self, resp, info): + self.body = None + if resp: + self.body = resp.read() + self.info = info + + @property + def json(self): + if not self.body: + if "body" in self.info: + return json.loads(self.info["body"]) + return None + try: + return json.loads(self.body) + except ValueError: + return None + + @property + def status_code(self): + return self.info["status"] + + +class Rest(object): + + def __init__(self, module, headers): + self.module = module + self.headers = headers + self.baseurl = 'https://api.digitalocean.com/v2' + + def _url_builder(self, path): + if path[0] == '/': + path = path[1:] + return '%s/%s' % (self.baseurl, path) + + def send(self, method, path, data=None, headers=None): + url = self._url_builder(path) + data = self.module.jsonify(data) + timeout = self.module.params['timeout'] + + resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method, timeout=timeout) + + # Exceptions in fetch_url may result in a status -1, the ensures a + if info['status'] == -1: + self.module.fail_json(msg=info['msg']) + + return Response(resp, info) + + def get(self, path, data=None, headers=None): + return self.send('GET', path, data, headers) + + def put(self, path, data=None, headers=None): + return self.send('PUT', path, data, headers) + + def post(self, path, data=None, headers=None): + return self.send('POST', path, data, headers) + + def delete(self, path, data=None, headers=None): + return self.send('DELETE', path, data, headers) + + +def wait_action(module, rest, ip, action_id, timeout=10): + end_time = time.time() + 10 + while time.time() < end_time: + response = rest.get('floating_ips/{0}/actions/{1}'.format(ip, action_id)) + status_code = response.status_code + status = response.json['action']['status'] + # TODO: check status_code == 200? + if status == 'completed': + return True + elif status == 'errored': + module.fail_json(msg='Floating ip action error [ip: {0}: action: {1}]'.format( + ip, action_id), data=json) + + module.fail_json(msg='Floating ip action timeout [ip: {0}: action: {1}]'.format( + ip, action_id), data=json) + + +def core(module): + api_token = module.params['oauth_token'] + state = module.params['state'] + ip = module.params['ip'] + droplet_id = module.params['droplet_id'] + + rest = Rest(module, {'Authorization': 'Bearer {0}'.format(api_token), + 'Content-type': 'application/json'}) + + if state in ('present'): + if droplet_id is not None and module.params['ip'] is not None: + # Lets try to associate the ip to the specified droplet + associate_floating_ips(module, rest) + else: + create_floating_ips(module, rest) + + elif state in ('absent'): + response = rest.delete("floating_ips/{0}".format(ip)) + status_code = response.status_code + json_data = response.json + if status_code == 204: + module.exit_json(changed=True) + elif status_code == 404: + module.exit_json(changed=False) + else: + module.exit_json(changed=False, data=json_data) + + +def get_floating_ip_details(module, rest): + ip = module.params['ip'] + + response = rest.get("floating_ips/{0}".format(ip)) + status_code = response.status_code + json_data = response.json + if status_code == 200: + return json_data['floating_ip'] + else: + module.fail_json(msg="Error assigning floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def assign_floating_id_to_droplet(module, rest): + ip = module.params['ip'] + + payload = { + "type": "assign", + "droplet_id": module.params['droplet_id'], + } + + response = rest.post("floating_ips/{0}/actions".format(ip), data=payload) + status_code = response.status_code + json_data = response.json + if status_code == 201: + wait_action(module, rest, ip, json_data['action']['id']) + + module.exit_json(changed=True, data=json_data) + else: + module.fail_json(msg="Error creating floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def associate_floating_ips(module, rest): + floating_ip = get_floating_ip_details(module, rest) + droplet = floating_ip['droplet'] + + # TODO: If already assigned to a droplet verify if is one of the specified as valid + if droplet is not None and str(droplet['id']) in [module.params['droplet_id']]: + module.exit_json(changed=False) + else: + assign_floating_id_to_droplet(module, rest) + + +def create_floating_ips(module, rest): + payload = { + } + floating_ip_data = None + + if module.params['region'] is not None: + payload["region"] = module.params['region'] + + if module.params['droplet_id'] is not None: + payload["droplet_id"] = module.params['droplet_id'] + + page = 1 + while page is not None: + response = rest.get('floating_ips?page={0}&per_page=20'.format(page)) + json_data = response.json + if response.status_code == 200: + for floating_ip in json_data['floating_ips']: + if floating_ip['droplet'] and floating_ip['droplet']['id'] == int(module.params['droplet_id']): + floating_ip_data = {'floating_ip': floating_ip} + if 'links' in json_data and 'pages' in json_data['links'] and 'next' in json_data['links']['pages']: + page += 1 + else: + page = None + + if floating_ip_data: + module.exit_json(changed=False, data=floating_ip_data) + else: + response = rest.post("floating_ips", data=payload) + status_code = response.status_code + json_data = response.json + + if status_code == 202: + module.exit_json(changed=True, data=json_data) + else: + module.fail_json(msg="Error creating floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(choices=['present', 'absent'], default='present'), + ip=dict(aliases=['id'], required=False), + region=dict(required=False), + droplet_id=dict(required=False), + oauth_token=dict( + no_log=True, + # Support environment variable for DigitalOcean OAuth Token + fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN']), + required=True, + ), + validate_certs=dict(type='bool', default=True), + timeout=dict(type='int', default=30), + ), + required_if=[ + ('state', 'delete', ['ip']) + ], + mutually_exclusive=[ + ['region', 'droplet_id'] + ], + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index ad81291..eabbb3e 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -1,11 +1,16 @@ --- +- name: Set the vpn endpoint as a fact + set_fact: + cloud_vpn_endpoint: "{{ cloud_static_ip if cloud_static_ip|ipaddr else cloud_instance_ip }}" + - name: Set subjectAltName as a fact set_fact: - IP_subject_alt_name: "{{ (IP_subject_alt_name if algo_provider == 'local' else cloud_instance_ip) | lower }}" + IP_subject_alt_name: "{{ (IP_subject_alt_name if algo_provider == 'local' else cloud_vpn_endpoint) | lower }}" + algo_server: "{{ 'localhost' if cloud_instance_ip == 'localhost' else cloud_instance_ip }}" - name: Add the server to an inventory group add_host: - name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + name: "{{ algo_server }}" groups: vpn-host ansible_connection: "{% if cloud_instance_ip == 'localhost' %}local{% else %}ssh{% endif %}" ansible_ssh_user: "{{ ansible_ssh_user }}" @@ -19,10 +24,11 @@ algo_ssh_tunneling: "{{ algo_ssh_tunneling }}" algo_store_pki: "{{ algo_store_pki }}" IP_subject_alt_name: "{{ IP_subject_alt_name }}" + static_ip: "{{ static_ip | default(omit) }}" - name: Additional variables for the server add_host: - name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + name: "{{ algo_server }}" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" when: algo_provider != 'local' diff --git a/playbooks/tmpfs/linux.yml b/playbooks/tmpfs/linux.yml index 64a9651..0f26ff9 100644 --- a/playbooks/tmpfs/linux.yml +++ b/playbooks/tmpfs/linux.yml @@ -1,5 +1,5 @@ --- - name: Linux | set OS specific facts set_fact: - tmpfs_volume_name: "AlgoVPN-{{ IP_subject_alt_name }}" + tmpfs_volume_name: "AlgoVPN-{{ algo_server }}" tmpfs_volume_path: /dev/shm diff --git a/playbooks/tmpfs/macos.yml b/playbooks/tmpfs/macos.yml index 72243da..625dbb5 100644 --- a/playbooks/tmpfs/macos.yml +++ b/playbooks/tmpfs/macos.yml @@ -1,7 +1,7 @@ --- - name: MacOS | set OS specific facts set_fact: - tmpfs_volume_name: "AlgoVPN-{{ IP_subject_alt_name }}" + tmpfs_volume_name: "AlgoVPN-{{ algo_server }}" tmpfs_volume_path: /Volumes - name: MacOS | mount a ram disk diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index b381525..d773f07 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -25,6 +25,15 @@ - Environment:Algo register: digital_ocean_droplet +- name: "Create a Floating IP" + digital_ocean_floating_ip: + state: present + oauth_token: "{{ algo_do_token }}" + droplet_id: "{{ digital_ocean_droplet.data.droplet.id }}" + register: digital_ocean_floating_ip + when: static_ip + - set_fact: cloud_instance_ip: "{{ digital_ocean_droplet.data.ip_address }}" + cloud_static_ip: "{{ digital_ocean_floating_ip.data.floating_ip.ip|default(omit) }}" ansible_ssh_user: root diff --git a/server.yml b/server.yml index 0eb7866..02ff11f 100644 --- a/server.yml +++ b/server.yml @@ -51,6 +51,7 @@ algo_dns_adblocking: {{ algo_dns_adblocking }} algo_ssh_tunneling: {{ algo_ssh_tunneling }} algo_store_pki: {{ algo_store_pki }} + static_ip: "{{ static_ip }}" IP_subject_alt_name: {{ IP_subject_alt_name }} ipsec_enabled: {{ ipsec_enabled }} wireguard_enabled: {{ wireguard_enabled }} @@ -63,7 +64,7 @@ - name: Create a symlink if deploying to localhost file: - src: "{{ IP_subject_alt_name }}" + src: "{{ algo_server }}" dest: configs/localhost state: link force: true