Skip to content
23 changes: 18 additions & 5 deletions hcloud/actions/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
# -*- coding: utf-8 -*-
from hcloud.core.client import ClientEntityBase, BoundModelBase
import time

from hcloud.actions.domain import Action
from hcloud.core.client import ClientEntityBase, BoundModelBase
from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException


class BoundAction(BoundModelBase):
model = Action

def wait_until_finished(self, max_retries=100):
while self.status == Action.STATUS_RUNNING:
if max_retries > 0:
self.reload()
time.sleep(self._client._client.poll_interval)
max_retries = max_retries - 1
else:
raise ActionTimeoutException(action=self)

if self.status == Action.STATUS_ERROR:
raise ActionFailedException(action=self)


class ActionsClient(ClientEntityBase):
results_list_attribute_name = 'actions'
Expand All @@ -17,9 +30,9 @@ def get_by_id(self, id):
return BoundAction(self, response['action'])

def get_list(self,
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundAction]]
Expand Down
14 changes: 14 additions & 0 deletions hcloud/actions/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@


class Action(BaseDomain):
STATUS_RUNNING = "running"
STATUS_SUCCESS = "success"
STATUS_ERROR = "error"

started = ISODateTime()
finished = ISODateTime()

Expand Down Expand Up @@ -36,3 +40,13 @@ def __init__(
self.finished = finished
self.resources = resources
self.error = error


class ActionFailedException(Exception):
def __init__(self, action):
self.action = action


class ActionTimeoutException(Exception):
def __init__(self, action):
self.action = action
3 changes: 2 additions & 1 deletion hcloud/hcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ class HcloudClient(object):
version = VERSION
retry_wait_time = 0.5

def __init__(self, token, api_endpoint="https://api.hetzner.cloud/v1", application_name=None, application_version=None):
def __init__(self, token, api_endpoint="https://api.hetzner.cloud/v1", application_name=None, application_version=None, poll_interval=1):
self.token = token
self._api_endpoint = api_endpoint
self._application_name = application_name
self._application_version = application_version
self.poll_interval = poll_interval

self.datacenters = DatacentersClient(self)
self.locations = LocationsClient(self)
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/actions/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,75 @@ def generic_action_list():
}
]
}


@pytest.fixture()
def running_action():
return {
"action": {
"id": 2,
"command": "stop_server",
"status": "running",
"progress": 100,
"started": "2016-01-30T23:55:00+00:00",
"finished": "2016-01-30T23:56:00+00:00",
"resources": [
{
"id": 42,
"type": "server"
}
],
"error": {
"code": "action_failed",
"message": "Action failed"
}
}
}


@pytest.fixture()
def successfully_action():
return {
"action": {
"id": 2,
"command": "stop_server",
"status": "success",
"progress": 100,
"started": "2016-01-30T23:55:00+00:00",
"finished": "2016-01-30T23:56:00+00:00",
"resources": [
{
"id": 42,
"type": "server"
}
],
"error": {
"code": "action_failed",
"message": "Action failed"
}
}
}


@pytest.fixture()
def failed_action():
return {
"action": {
"id": 2,
"command": "stop_server",
"status": "error",
"progress": 100,
"started": "2016-01-30T23:55:00+00:00",
"finished": "2016-01-30T23:56:00+00:00",
"resources": [
{
"id": 42,
"type": "server"
}
],
"error": {
"code": "action_failed",
"message": "Action failed"
}
}
}
32 changes: 31 additions & 1 deletion tests/unit/actions/test_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
import mock
import pytest

from hcloud.actions.client import ActionsClient
from hcloud.actions.client import ActionsClient, BoundAction
from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException


class TestBoundAction(object):
@pytest.fixture()
def bound_running_action(self, mocked_requests):
return BoundAction(client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATUS_RUNNING))

def test_wait_until_finished(self, bound_running_action, mocked_requests, running_action, successfully_action):
mocked_requests.request.side_effect = [running_action, successfully_action]
bound_running_action.wait_until_finished()
assert bound_running_action.status == "success"
assert mocked_requests.request.call_count == 2

def test_wait_until_finished_with_error(self, bound_running_action, mocked_requests, running_action, failed_action):
mocked_requests.request.side_effect = [running_action, failed_action]
with pytest.raises(ActionFailedException) as exception_info:
bound_running_action.wait_until_finished()

assert bound_running_action.status == "error"
assert exception_info.value.action.id == 2

def test_wait_until_finished_max_retries(self, bound_running_action, mocked_requests, running_action, successfully_action):
mocked_requests.request.side_effect = [running_action, running_action, successfully_action]
with pytest.raises(ActionTimeoutException) as exception_info:
bound_running_action.wait_until_finished(max_retries=1)

assert bound_running_action.status == "running"
assert exception_info.value.action.id == 2
assert mocked_requests.request.call_count == 1


class TestActionsClient(object):
Expand Down