diff --git a/config.cfg b/config.cfg index bf65e45..6b5358b 100644 --- a/config.cfg +++ b/config.cfg @@ -160,6 +160,10 @@ cloud_providers: openstack: flavor_ram: ">=512" image: Ubuntu-18.04 + cloudstack: + size: Micro + image: Linux Ubuntu 19.04 64-bit + disk: 10 vultr: os: Ubuntu 19.04 x64 size: 1024 MB RAM,25 GB SSD,1.00 TB BW diff --git a/docs/cloud-cloudstack.md b/docs/cloud-cloudstack.md new file mode 100644 index 0000000..e9cd3c6 --- /dev/null +++ b/docs/cloud-cloudstack.md @@ -0,0 +1,20 @@ +### Configuration file + +You need to create a configuration file in INI format with your api key in `$HOME/.cloudstack.ini` + +``` +[cloudstack] +endpoint = +key = +secret = +timeout = 30 +``` + +Example for Exoscale (European cloud provider exposing CloudStack API), visit https://portal.exoscale.com/u//account/profile/api to gather the required information: +``` +[exoscale] +endpoint = https://api.exoscale.com/compute +key = +secret = +timeout = 30 +``` diff --git a/docs/index.md b/docs/index.md index 118a655..b0e8213 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,7 @@ - Configure [DigitalOcean](cloud-do.md) - Configure [Google Cloud Platform](cloud-gce.md) - Configure [Vultr](cloud-vultr.md) + - Configure [CloudStack](cloud-cloudstack.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - Deploy to your own [Ubuntu](deploy-to-ubuntu.md) server diff --git a/input.yml b/input.yml index bfabc85..cf0e9eb 100644 --- a/input.yml +++ b/input.yml @@ -19,6 +19,7 @@ - { name: Google Compute Engine, alias: gce } - { name: Scaleway, alias: scaleway} - { name: OpenStack (DreamCompute optimised), alias: openstack } + - { name: CloudStack (Exoscale optimised), alias: cloudstack } - { name: Install to existing Ubuntu 18.04 or 19.04 server (Advanced), alias: local } vars_files: - config.cfg diff --git a/library/cloudstack_zones.py b/library/cloudstack_zones.py new file mode 100644 index 0000000..3e5d9a0 --- /dev/null +++ b/library/cloudstack_zones.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudstack import ( + AnsibleCloudStack, + cs_argument_spec, + cs_required_together, +) + +DOCUMENTATION = ''' +--- +module: cloudstack_zones +short_description: List zones on Apache CloudStack based clouds. +description: + - List zones. +version_added: '0.1' +author: Julien Bachmann (@0xmilkmix) +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +- name: List zones + cloudstack_zones: + register: _cs_zones +''' + +RETURN = ''' +--- +zone: + description: List of zones. + returned: success + type: list + sample: + [ + { + "allocationstate": "Enabled", + "dhcpprovider": "VirtualRouter", + "id": "", + "localstorageenabled": true, + "name": "ch-gva-2", + "networktype": "Basic", + "securitygroupsenabled": true, + "tags": [], + "zonetoken": "token" + }, + { + "allocationstate": "Enabled", + "dhcpprovider": "VirtualRouter", + "id": "", + "localstorageenabled": true, + "name": "ch-dk-2", + "networktype": "Basic", + "securitygroupsenabled": true, + "tags": [], + "zonetoken": "token" + }, + { + "allocationstate": "Enabled", + "dhcpprovider": "VirtualRouter", + "id": "", + "localstorageenabled": true, + "name": "at-vie-1", + "networktype": "Basic", + "securitygroupsenabled": true, + "tags": [], + "zonetoken": "token" + }, + { + "allocationstate": "Enabled", + "dhcpprovider": "VirtualRouter", + "id": "", + "localstorageenabled": true, + "name": "de-fra-1", + "networktype": "Basic", + "securitygroupsenabled": true, + "tags": [], + "zonetoken": "token" + } + ] +''' + +class AnsibleCloudStackZones(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackZones, self).__init__(module) + self.zones = None + + def get_zones(self): + args = {} + if not self.zones: + zones = self.query_api('listZones', **args) + if zones: + self.zones = zones + return self.zones + +def main(): + module = AnsibleModule(argument_spec={}) + acs_zones = AnsibleCloudStackZones(module) + result = acs_zones.get_zones() + module.exit_json(**result) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/roles/cloud-cloudstack/defaults/main.yml b/roles/cloud-cloudstack/defaults/main.yml new file mode 100644 index 0000000..b3f545d --- /dev/null +++ b/roles/cloud-cloudstack/defaults/main.yml @@ -0,0 +1,2 @@ +--- +cloudstack_venv: "{{ playbook_dir }}/configs/.venvs/cloudstack" diff --git a/roles/cloud-cloudstack/tasks/main.yml b/roles/cloud-cloudstack/tasks/main.yml new file mode 100644 index 0000000..e370e02 --- /dev/null +++ b/roles/cloud-cloudstack/tasks/main.yml @@ -0,0 +1,71 @@ +--- +- block: + - name: Build python virtual environment + import_tasks: venv.yml + + - name: Include prompts + import_tasks: prompts.yml + + - block: + - set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input | length > 0 %}{{ cs_zones[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ cs_zones[default_zone | int - 1]['name'] }}{% endif %} + + - name: Security group created + cs_securitygroup: + name: "{{ algo_server_name }}-security_group" + description: AlgoVPN security group + register: cs_security_group + + - name: Security rules created + cs_securitygroup_rule: + security_group: "{{ cs_security_group.name }}" + protocol: "{{ item.proto }}" + start_port: "{{ item.start_port }}" + end_port: "{{ item.end_port }}" + cidr: "{{ item.range }}" + with_items: + - { proto: tcp, start_port: 22, end_port: 22, range: 0.0.0.0/0 } + - { proto: udp, start_port: 4500, end_port: 4500, range: 0.0.0.0/0 } + - { proto: udp, start_port: 500, end_port: 500, range: 0.0.0.0/0 } + - { proto: udp, start_port: "{{ wireguard_port }}", end_port: "{{ wireguard_port }}", range: 0.0.0.0/0 } + + - name: Keypair created + cs_sshkeypair: + name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + register: cs_keypair + + - name: Set facts + set_fact: + image_id: "{{ cloud_providers.cloudstack.image }}" + size: "{{ cloud_providers.cloudstack.size }}" + disk: "{{ cloud_providers.cloudstack.disk }}" + keypair_name: "{{ cs_keypair.name }}" + + - name: Server created + cs_instance: + name: "{{ algo_server_name }}" + root_disk_size: "{{ disk }}" + template: "{{ image_id }}" + ssh_key: "{{ keypair_name }}" + security_groups: "{{ cs_security_group.name }}" + zone: "{{ algo_region }}" + service_offering: "{{ size }}" + register: cs_server + + - set_fact: + cloud_instance_ip: "{{ cs_server.default_ip }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ cloudstack_venv }}/lib/python2.7/site-packages/" + CLOUDSTACK_CONFIG: "{{ algo_cs_config }}" + CLOUDSTACK_REGION: "{{ algo_cs_region }}" + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-cloudstack/tasks/prompts.yml b/roles/cloud-cloudstack/tasks/prompts.yml new file mode 100644 index 0000000..62812be --- /dev/null +++ b/roles/cloud-cloudstack/tasks/prompts.yml @@ -0,0 +1,55 @@ +--- +- block: + - pause: + prompt: | + Enter path for cloudstack.ini file (https://trailofbits.github.io/algo/cloud-cloudstack.html) + [~/.cloudstack.ini] + register: _cs_config + when: + - cs_config is undefined + - lookup('env', 'CLOUDSTACK_CONFIG') | length <= 0 + + - pause: + prompt: | + Specify region to use in cloudstack.ini file + [exoscale] + register: _cs_region + when: + - cs_region is undefined + - lookup('env', 'CLOUDSTACK_REGION') | length <= 0 + + - set_fact: + algo_cs_config: "{{ cs_config | default(_cs_config.user_input|default(None)) | default(lookup('env', 'CLOUDSTACK_CONFIG'), true) | default('~/.cloudstack.ini', true) }}" + algo_cs_region: "{{ cs_region | default(_cs_region.user_input|default(None)) | default(lookup('env', 'CLOUDSTACK_REGION'), true) | default('exoscale', true) }}" + + - name: Get zones on cloud + cloudstack_zones: + register: _cs_zones + environment: + CLOUDSTACK_CONFIG: "{{ algo_cs_config }}" + CLOUDSTACK_REGION: "{{ algo_cs_region }}" + + - name: Extract zones from output + set_fact: + cs_zones: "{{ _cs_zones['zone'] | sort(attribute='name') }}" + + - name: Set the default zone + set_fact: + default_zone: >- + {% for z in cs_zones %} + {%- if z['name'] == "ch-gva-2" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What zone should the server be located in? + {% for z in cs_zones %} + {{ loop.index }}. {{ z['name'] }} + {% endfor %} + + Enter the number of your desired zone + [{{ default_zone }}] + register: _algo_region + when: region is undefined + environment: + PYTHONPATH: "{{ cloudstack_venv }}/lib/python2.7/site-packages/" diff --git a/roles/cloud-cloudstack/tasks/venv.yml b/roles/cloud-cloudstack/tasks/venv.yml new file mode 100644 index 0000000..cffda77 --- /dev/null +++ b/roles/cloud-cloudstack/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ cloudstack_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - cs + - sshpubkeys + state: latest + virtualenv: "{{ cloudstack_venv }}" + virtualenv_python: python2.7