Add hcloud server module (#53062)

This commit is contained in:
Lukas Kämmerling 2019-03-05 07:30:24 +01:00 committed by René Moser
parent bdfa99af9f
commit 66beeaf032
9 changed files with 832 additions and 0 deletions

View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.ansible_release import __version__
from ansible.module_utils.basic import env_fallback, missing_required_lib
try:
import hcloud
HAS_HCLOUD = True
except ImportError:
HAS_HCLOUD = False
class Hcloud(object):
def __init__(self, module, represent):
self.module = module
self.represent = represent
self.result = {"changed": False, self.represent: None}
if not HAS_HCLOUD:
module.fail_json(msg=missing_required_lib("hcloud-python"))
self._build_client()
def _build_client(self):
self.client = hcloud.Client(
token=self.module.params["api_token"],
api_endpoint=self.module.params["endpoint"],
application_name="ansible-module",
application_version=__version__,
)
def _mark_as_changed(self):
self.result["changed"] = True
@staticmethod
def base_module_arguments():
return {
"api_token": {
"type": "str",
"required": True,
"fallback": (env_fallback, ["HCLOUD_TOKEN"]),
"no_log": True,
},
"endpoint": {"type": "str", "default": "https://api.hetzner.cloud/v1"},
}
def _prepare_result(self):
"""Prepare the result for every module
:return: dict
"""
return {}
def get_result(self):
if getattr(self, self.represent) is not None:
self.result[self.represent] = self._prepare_result()
return self.result

View file

@ -0,0 +1,380 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# 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: hcloud_server
short_description: Create and manage cloud servers on the Hetzner Cloud.
version_added: "2.8"
description:
- Create, update and manage cloud servers on the Hetzner Cloud.
author:
- Lukas Kaemmerling (@lkaemmerling)
options:
id:
description:
- The ID of the Hetzner Cloud server to manage.
- Only required if no server I(name) is given
type: int
name:
description:
- The Name of the Hetzner Cloud server to manage.
- Only required if no server I(id) is given or a server does not exists.
type: str
server_type:
description:
- The Server Type of the Hetzner Cloud server to manage.
- Required if server does not exists.
type: str
ssh_keys:
description:
- List of SSH Keys Names
type: list
volumes:
description:
- List of Volumes IDs that should be attached to the server on server creation.
type: list
image:
description:
- Image the server should be created from.
- Required if server does not exists.
type: str
location:
description:
- Location of Server.
- Required if no I(datacenter) is given and server does not exists.
type: str
datacenter:
description:
- Datacenter of Server.
- Required of no I(location) is given and server does not exists.
type: str
backups:
description:
- Enable or disable Backups for the given Server.
type: bool
default: no
upgrade_disk:
description:
- Resize the disk size, when resizing a server.
- If you want to downgrade the server later, this value should be False.
type: bool
default: no
force_upgrade:
description:
- Force the upgrade of the server.
- Power off the server if it is running on upgrade.
type: bool
default: no
user_data:
description:
- User Data to be passed to the server on creation.
- Only used if server does not exists.
type: str
state:
description:
- State of the server.
default: present
choices: [ absent, present, restarted, started, stopped ]
type: str
extends_documentation_fragment: hcloud
"""
EXAMPLES = """
- name: Create a basic server
hcloud_server:
name: my-server
server_type: cx11
image: ubuntu-18.04
state: present
- name: Create a basic server with ssh key
hcloud_server:
name: my-server
server_type: cx11
image: ubuntu-18.04
location: fsn1
ssh-key: my-ssh-key
state: present
- name: Resize an existing server
hcloud_server:
name: my-server
server_type: cx21
keep_disk: yes
state: present
- name: Ensure the server is absent (remove if needed)
hcloud_server:
name: my-server
state: absent
- name: Ensure the server is started
hcloud_server:
name: my-server
state: started
- name: Ensure the server is stopped
hcloud_server:
name: my-server
state: stopped
- name: Ensure the server is restarted
hcloud_server:
name: my-server
state: restarted
"""
RETURN = """
hcloud_server:
description: The server instance
returned: Always
type: dict
sample: {
"backup_window": null,
"datacenter": "nbg1-dc3",
"id": 1937415,
"image": "ubuntu-18.04",
"ipv4_address": "116.203.104.109",
"ipv6": "2a01:4f8:1c1c:c140::/64",
"labels": {},
"location": "nbg1",
"name": "mein-server-2",
"rescue_enabled": false,
"server_type": "cx11",
"status": "running"
}
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.hcloud import Hcloud
try:
from hcloud.volumes.domain import Volume
from hcloud.ssh_keys.domain import SSHKey
from hcloud.servers.domain import Server
from hcloud import APIException
except ImportError:
pass
class AnsibleHcloudServer(Hcloud):
def __init__(self, module):
Hcloud.__init__(self, module, "hcloud_server")
self.hcloud_server = None
def _prepare_result(self):
return {
"id": to_native(self.hcloud_server.id),
"name": to_native(self.hcloud_server.name),
"ipv4_address": to_native(self.hcloud_server.public_net.ipv4.ip),
"ipv6": to_native(self.hcloud_server.public_net.ipv6.ip),
"image": to_native(self.hcloud_server.image.name),
"server_type": to_native(self.hcloud_server.server_type.name),
"datacenter": to_native(self.hcloud_server.datacenter.name),
"location": to_native(self.hcloud_server.datacenter.location.name),
"rescue_enabled": self.hcloud_server.rescue_enabled,
"backup_window": to_native(self.hcloud_server.backup_window),
"labels": self.hcloud_server.labels,
"status": to_native(self.hcloud_server.status),
}
def _get_server(self):
try:
if self.module.params.get("id") is not None:
self.hcloud_server = self.client.servers.get_by_id(
self.module.params.get("id")
)
else:
self.hcloud_server = self.client.servers.get_by_name(
self.module.params.get("name")
)
except APIException as e:
self.module.fail_json(msg=e.message)
def _create_server(self):
self.module.fail_on_missing_params(
required_params=["name", "server_type", "image"]
)
params = {
"name": self.module.params.get("name"),
"server_type": self.client.server_types.get_by_name(
self.module.params.get("server_type")
),
"image": self.client.images.get_by_name(self.module.params.get("image")),
"user_data": self.module.params.get("user_data"),
}
if self.module.params.get("ssh_keys") is not None:
params["ssh_keys"] = [
SSHKey(name=ssh_key_name)
for ssh_key_name in self.module.params.get("ssh_keys")
]
if self.module.params.get("volumes") is not None:
params["volumes"] = [
Volume(id=volume_id) for volume_id in self.module.params.get("volumes")
]
if self.module.params.get("location") is None and self.module.params.get("datacenter") is None:
# When not given, the API will choose the location.
params["location"] = None
params["datacenter"] = None
elif self.module.params.get("location") is not None and self.module.params.get("datacenter") is None:
params["location"] = self.client.locations.get_by_name(
self.module.params.get("location")
)
elif self.module.params.get("location") is None and self.module.params.get("datacenter") is not None:
params["datacenter"] = self.client.datacenters.get_by_name(
self.module.params.get("datacenter")
)
if not self.module.check_mode:
resp = self.client.servers.create(**params)
self.result["root_password"] = resp.root_password
resp.action.wait_until_finished()
[action.wait_until_finished() for action in resp.next_actions]
self._mark_as_changed()
self._get_server()
def _update_server(self):
if self.module.params.get("backups") and self.hcloud_server.backup_window is None:
if not self.module.check_mode:
self.hcloud_server.enable_backup().wait_until_finished()
self._mark_as_changed()
elif not self.module.params.get("backups") and self.hcloud_server.backup_window is not None:
if not self.module.check_mode:
self.hcloud_server.disable_backup().wait_until_finished()
self._mark_as_changed()
server_type = self.module.params.get("server_type")
if server_type is not None and self.hcloud_server.server_type.name != server_type:
previous_server_status = self.hcloud_server.status
state = self.module.params.get("state")
if previous_server_status == Server.STATUS_RUNNING:
if not self.module.check_mode:
if self.module.params.get("force_upgrade") or state == "stopped":
self.stop_server() # Only stopped server can be upgraded
else:
self.module.warn(
"You can not upgrade a running instance %s. You need to stop the instance or use force_upgrade=yes."
% self.hcloud_server.name
)
timeout = 100
if self.module.params.get("upgrade_disk"):
timeout = (
500
) # When we upgrade the disk too the resize progress takes some more time.
if not self.module.check_mode:
self.hcloud_server.change_type(
server_type=self.client.server_types.get_by_name(server_type),
upgrade_disk=self.module.params.get("upgrade_disk"),
).wait_until_finished(timeout)
if state == "present" and previous_server_status == Server.STATUS_RUNNING or state == "started":
self.start_server()
self._mark_as_changed()
self._get_server()
def start_server(self):
if self.hcloud_server.status != Server.STATUS_RUNNING:
if not self.module.check_mode:
self.client.servers.power_on(self.hcloud_server).wait_until_finished()
self._mark_as_changed()
self._get_server()
def stop_server(self):
if self.hcloud_server.status != Server.STATUS_OFF:
if not self.module.check_mode:
self.client.servers.power_off(self.hcloud_server).wait_until_finished()
self._mark_as_changed()
self._get_server()
def present_server(self):
self._get_server()
if self.hcloud_server is None:
self._create_server()
else:
self._update_server()
def delete_server(self):
self._get_server()
if self.hcloud_server is not None:
if not self.module.check_mode:
self.client.servers.delete(self.hcloud_server).wait_until_finished()
self._mark_as_changed()
self.hcloud_server = None
@staticmethod
def define_module():
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
image={"type": "str"},
server_type={"type": "str"},
location={"type": "str"},
datacenter={"type": "str"},
user_data={"type": "str"},
ssh_keys={"type": "list"},
volumes={"type": "list"},
backups={"type": "bool", "default": False},
upgrade_disk={"type": "bool", "default": False},
force_upgrade={"type": "bool", "default": False},
state={
"choices": ["absent", "present", "restarted", "started", "stopped"],
"default": "present",
},
**Hcloud.base_module_arguments()
),
required_one_of=[['id', 'name']],
mutually_exclusive=[["location", "datacenter"]],
supports_check_mode=True,
)
def main():
module = AnsibleHcloudServer.define_module()
hcloud = AnsibleHcloudServer(module)
state = module.params.get("state")
if state == "absent":
hcloud.delete_server()
elif state == "present":
hcloud.present_server()
elif state == "started":
hcloud.present_server()
hcloud.start_server()
elif state == "stopped":
hcloud.present_server()
hcloud.stop_server()
elif state == "restarted":
hcloud.present_server()
hcloud.stop_server()
hcloud.start_server()
module.exit_json(**hcloud.get_result())
if __name__ == "__main__":
main()

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
class ModuleDocFragment(object):
DOCUMENTATION = '''
options:
api_token:
description:
- This is the API Token for the Hetzner Cloud.
required: True
type: str
endpoint:
description:
- This is the API Endpoint for the Hetzner Cloud.
default: https://api.hetzner.cloud/v1
type: str
requirements:
- hcloud-python >= 1.0.0
seealso:
- name: Documentation for Hetzner Cloud API
description: Complete reference for the Hetzner Cloud API.
link: https://docs.hetzner.cloud/
'''

View file

@ -0,0 +1,12 @@
# This is the configuration template for ansible-test Hetzner Cloud integration tests.
#
# You do not need this template if you are:
#
# 1) Running integration tests without using ansible-test.
#
# If you want to test against the Hetzner Cloud public API,
# fill in the values below and save this file without the .template extension.
# This will cause ansible-test to use the given configuration.
[default]
hcloud_api_token= @TOKEN

View file

@ -0,0 +1,2 @@
cloud/hcloud
unsupported

View file

@ -0,0 +1,5 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
hcloud_prefix: "tests"
hcloud_server_name: "{{hcloud_prefix}}-integration"

View file

@ -0,0 +1,275 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
---
- name: setup
hcloud_server:
name: "{{ hcloud_server_name }}"
state: absent
register: result
- name: verify setup
assert:
that:
- result is success
- name: test missing required parameters on create server
hcloud_server:
name: "{{ hcloud_server_name }}"
register: result
ignore_errors: yes
- name: verify fail test missing required parameters on create server
assert:
that:
- result is failed
- 'result.msg == "missing required arguments: server_type, image"'
- name: test create server with check mode
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: cx11
image: ubuntu-18.04
state: present
register: result
check_mode: yes
- name: test create server server
assert:
that:
- result is changed
- name: test create server
hcloud_server:
name: "{{ hcloud_server_name}}"
server_type: cx11
image: ubuntu-18.04
state: started
register: main_server
- name: verify create server
assert:
that:
- main_server is changed
- main_server.hcloud_server.name == "{{ hcloud_server_name }}"
- main_server.hcloud_server.server_type == "cx11"
- main_server.hcloud_server.status == "running"
- main_server.root_password != ""
- name: test create server idempotence
hcloud_server:
name: "{{ hcloud_server_name }}"
state: started
register: result
- name: verify create server idempotence
assert:
that:
- result is not changed
- name: test stop server with check mode
hcloud_server:
name: "{{ hcloud_server_name }}"
state: stopped
register: result
check_mode: yes
- name: verify stop server with check mode
assert:
that:
- result is changed
- result.hcloud_server.status == "running"
- name: test stop server
hcloud_server:
name: "{{ hcloud_server_name }}"
state: stopped
register: result
- name: verify stop server
assert:
that:
- result is changed
- result.hcloud_server.status == "off"
- name: test start server with check mode
hcloud_server:
name: "{{ hcloud_server_name }}"
state: started
register: result
check_mode: true
- name: verify start server with check mode
assert:
that:
- result is changed
- name: test start server
hcloud_server:
name: "{{ hcloud_server_name }}"
state: started
register: result
- name: verify start server
assert:
that:
- result is changed
- result.hcloud_server.status == "running"
- name: test start server idempotence
hcloud_server:
name: "{{ hcloud_server_name }}"
state: started
register: result
- name: verify start server idempotence
assert:
that:
- result is not changed
- result.hcloud_server.status == "running"
- name: test stop server by its id
hcloud_server:
id: "{{ main_server.hcloud_server.id }}"
state: stopped
register: result
- name: verify stop server by its id
assert:
that:
- result is changed
- result.hcloud_server.status == "off"
- name: test resize server running without force
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx21"
state: present
register: result
check_mode: true
- name: verify test resize server running without force
assert:
that:
- result is changed
- result.hcloud_server.server_type == "cx11"
- name: test resize server with check mode
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx21"
state: stopped
register: result
check_mode: true
- name: verify resize server with check mode
assert:
that:
- result is changed
- name: test resize server without disk
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx21"
state: stopped
register: result
- name: verify resize server without disk
assert:
that:
- result is changed
- result.hcloud_server.server_type == "cx21"
- name: test resize server idempotence
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx21"
state: stopped
register: result
- name: verify resize server idempotence
assert:
that:
- result is not changed
- name: test resize server to smaller plan
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx11"
state: stopped
register: result
- name: verify resize server to smaller plan
assert:
that:
- result is changed
- result.hcloud_server.server_type == "cx11"
- name: test resize server with disk
hcloud_server:
name: "{{ hcloud_server_name }}"
server_type: "cx21"
upgrade_disk: true
state: stopped
register: result
- name: verify resize server with disk
assert:
that:
- result is changed
- result.hcloud_server.server_type == "cx21"
- name: test enable backups with check mode
hcloud_server:
name: "{{ hcloud_server_name }}"
backups: true
state: stopped
register: result
check_mode: true
- name: verify enable backups with check mode
assert:
that:
- result is changed
- name: test enable backups
hcloud_server:
name: "{{ hcloud_server_name }}"
backups: true
state: stopped
register: result
- name: verify enable backups
assert:
that:
- result is changed
- result.hcloud_server.backup_window != ""
- name: test enable backups idempotence
hcloud_server:
name: "{{ hcloud_server_name }}"
backups: true
state: stopped
register: result
- name: verify enable backups idempotence
assert:
that:
- result is not changed
- result.hcloud_server.backup_window != ""
- name: absent server
hcloud_server:
name: "{{ hcloud_server_name }}"
state: absent
register: result
- name: verify absent server
assert:
that:
- result is success
- name: test create server with ssh key
hcloud_server:
name: "{{ hcloud_server_name}}"
server_type: cx11
image: "ubuntu-18.04"
ssh_keys:
- ci@ansible.hetzner.cloud
state: started
register: main_server
- name: verify create server
assert:
that:
- main_server is changed
- main_server.hcloud_server.name == "{{ hcloud_server_name }}"
- main_server.hcloud_server.server_type == "cx11"
- main_server.hcloud_server.status == "running"
- main_server.root_password != ""
- name: cleanup
hcloud_server:
name: "{{ hcloud_server_name }}"
state: absent
register: result
- name: verify cleanup
assert:
that:
- result is success

View file

@ -0,0 +1,69 @@
"""Hetzner Cloud plugin for integration tests."""
from __future__ import absolute_import, print_function
from os.path import isfile
from lib.cloud import (
CloudProvider,
CloudEnvironment,
CloudEnvironmentConfig,
)
from lib.util import ConfigParser, display
class HcloudCloudProvider(CloudProvider):
"""Hetzner Cloud provider plugin. Sets up cloud resources before
delegation.
"""
def __init__(self, args):
"""
:type args: TestConfig
"""
super(HcloudCloudProvider, self).__init__(args)
def filter(self, targets, exclude):
"""Filter out the cloud tests when the necessary config and resources are not available.
:type targets: tuple[TestTarget]
:type exclude: list[str]
"""
if isfile(self.config_static_path):
return
super(HcloudCloudProvider, self).filter(targets, exclude)
def setup(self):
"""Setup the cloud resource before delegation and register a cleanup callback."""
super(HcloudCloudProvider, self).setup()
if isfile(self.config_static_path):
self.config_path = self.config_static_path
return True
return False
class HcloudCloudEnvironment(CloudEnvironment):
"""Hetzner Cloud cloud environment plugin. Updates integration test environment
after delegation.
"""
def get_environment_config(self):
parser = ConfigParser()
parser.read(self.config_path)
env_vars = dict(
HCLOUD_TOKEN=parser.get('default', 'hcloud_api_token'),
)
ansible_vars = dict(
hcloud_prefix=self.resource_prefix,
)
ansible_vars.update(dict((key.lower(), value) for key, value in env_vars.items()))
return CloudEnvironmentConfig(
env_vars=env_vars,
ansible_vars=ansible_vars,
)