Example #1
0
    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")),
            "user_data":
            self.module.params.get("user_data"),
            "labels":
            self.module.params.get("labels"),
        }
        if self.client.images.get_by_name(
                self.module.params.get("image")) is not None:
            # When image name is not available look for id instead
            params["image"] = self.client.images.get_by_name(
                self.module.params.get("image"))
        else:
            params["image"] = self.client.images.get_by_id(
                self.module.params.get("image"))

        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(max_retries=1000)
            [action.wait_until_finished() for action in resp.next_actions]
        self._mark_as_changed()
        self._get_server()
Example #2
0
    def test_create(self, hetzner_client):
        response = hetzner_client.servers.create(
            "my-server",
            server_type=ServerType(name="cx11"),
            image=Image(name="ubuntu-16.04"),
            ssh_keys=[SSHKey(name="my-ssh-key")],
            volumes=[Volume(id=1)],
            user_data=
            "#cloud-config\\nruncmd:\\n- [touch, /root/cloud-init-worked]\\n",
            location=Location(name="nbg1"),
            automount=False)
        server = response.server
        action = response.action
        next_actions = response.next_actions
        root_password = response.root_password

        assert server.id == 42
        assert server.volumes == []
        assert server.server_type.id == 1
        assert server.datacenter.id == 1
        assert server.image.id == 4711

        assert action.id == 1
        assert action.command == "create_server"

        assert len(next_actions) == 1
        assert next_actions[0].id == 13
        assert next_actions[0].command == "start_server"

        assert root_password == "YItygq1v3GYjjMomLaKc"
Example #3
0
 def realise_resize_volume(self, allow_recreate: bool) -> None:
     defn: VolumeOptions = self.get_defn().config
     size: int = self._state["size"]
     if size > defn.size:
         raise Exception("decreasing a volume's size isn't supported.")
     elif size < defn.size:
         self.logger.log(
             f"increasing volume size from {size} GiB to {defn.size} GiB")
         self.get_client().volumes.resize(
             volume=Volume(self.resource_id),
             size=defn.size).wait_until_finished()
         with self.depl._db:
             self._state["size"] = defn.size
             self.needsFSResize = True
Example #4
0
 async def stop(self):
     if self.running == True:
         print("Stopping " + self.name)
         self.client.servers.shutdown(self.server)
         time.sleep(30)
         while self.server.status != Server.STATUS_OFF:
             print(self.server.status)
             time.sleep(5)
             serv = self.client.servers.get_by_id(self.server.id)
             self.server = serv
         print("Server is now stopped")
         time.sleep(1)
         self.client.volumes.detach(Volume(self.volume))
         time.sleep(1)
         self.client.servers.delete(self.server)
         self.running = False
         self.server = None
     else:
         print(self.name + " isn't running")
Example #5
0
 async def start(self):
     if self.running == False:
         print("Starting " + self.name)
         response = self.client.servers.create(
             self.name,
             server_type=ServerType(name=self.servertype),
             image=Image(self.snapshot),
             location=Location(self.location),
             volumes=[Volume(self.volume)])
         self.server = response.server
         #while self.server.status != Server.STATUS_RUNNING:
         #    print(self.server.status)
         #    time.sleep(2)
         #    serv = self.client.servers.get_by_id(self.server.id)
         #    self.server = serv
         #print("Server is now running")
         self.running = True
     else:
         print(self.name + " is already running")
Example #6
0
    def test_create_with_volumes(self, servers_client, response_create_simple_server):
        servers_client._client.request.return_value = response_create_simple_server
        volumes = [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=2))]
        response = servers_client.create(
            "my-server",
            server_type=ServerType(name="cx11"),
            image=Image(id=4711),
            volumes=volumes,
            start_after_create=False
        )
        servers_client._client.request.assert_called_with(
            url="/servers",
            method="POST",
            json={
                'name': "my-server",
                'server_type': "cx11",
                'image': 4711,
                'volumes': [1, 2],
                "start_after_create": False
            }
        )

        bound_server = response.server
        bound_action = response.action
        next_actions = response.next_actions
        root_password = response.root_password

        assert root_password == "YItygq1v3GYjjMomLaKc"

        assert bound_server._client is servers_client
        assert bound_server.id == 1
        assert bound_server.name == "my-server"

        assert isinstance(bound_action, BoundAction)
        assert bound_action._client == servers_client._client.actions
        assert bound_action.id == 1
        assert bound_action.command == "create_server"

        assert next_actions[0].id == 13
Example #7
0
class TestVolumesClient(object):

    @pytest.fixture()
    def volumes_client(self):
        return VolumesClient(client=mock.MagicMock())

    def test_get_by_id(self, volumes_client, volume_response):
        volumes_client._client.request.return_value = volume_response
        bound_volume = volumes_client.get_by_id(1)
        volumes_client._client.request.assert_called_with(url="/volumes/1", method="GET")
        assert bound_volume._client is volumes_client
        assert bound_volume.id == 1
        assert bound_volume.name == "database-storage"

    @pytest.mark.parametrize(
        "params",
        [
            {'label_selector': "label1", 'page': 1, 'per_page': 10},
            {}
        ]
    )
    def test_get_list(self, volumes_client, two_volumes_response, params):
        volumes_client._client.request.return_value = two_volumes_response
        result = volumes_client.get_list(**params)
        volumes_client._client.request.assert_called_with(url="/volumes", method="GET", params=params)

        bound_volumes = result.volumes
        assert result.meta is None

        assert len(bound_volumes) == 2

        bound_volume1 = bound_volumes[0]
        bound_volume2 = bound_volumes[1]

        assert bound_volume1._client is volumes_client
        assert bound_volume1.id == 1
        assert bound_volume1.name == "database-storage"

        assert bound_volume2._client is volumes_client
        assert bound_volume2.id == 2
        assert bound_volume2.name == "vault-storage"

    @pytest.mark.parametrize("params", [{'label_selector': "label1"}])
    def test_get_all(self, volumes_client, two_volumes_response, params):
        volumes_client._client.request.return_value = two_volumes_response
        bound_volumes = volumes_client.get_all(**params)

        params.update({'page': 1, 'per_page': 50})

        volumes_client._client.request.assert_called_with(url="/volumes", method="GET", params=params)

        assert len(bound_volumes) == 2

        bound_volume1 = bound_volumes[0]
        bound_volume2 = bound_volumes[1]

        assert bound_volume1._client is volumes_client
        assert bound_volume1.id == 1
        assert bound_volume1.name == "database-storage"

        assert bound_volume2._client is volumes_client
        assert bound_volume2.id == 2
        assert bound_volume2.name == "vault-storage"

    def test_get_by_name(self, volumes_client, one_volumes_response):
        volumes_client._client.request.return_value = one_volumes_response
        bound_volume = volumes_client.get_by_name("database-storage")

        params = {'name': "database-storage"}

        volumes_client._client.request.assert_called_with(url="/volumes", method="GET", params=params)

        assert bound_volume._client is volumes_client
        assert bound_volume.id == 1
        assert bound_volume.name == "database-storage"

    def test_create_with_location(self, volumes_client, volume_create_response):
        volumes_client._client.request.return_value = volume_create_response
        response = volumes_client.create(
            100,
            "database-storage",
            location=Location(name="location"),
            automount=False,
            format="xfs"
        )
        volumes_client._client.request.assert_called_with(
            url="/volumes",
            method="POST",
            json={
                'name': "database-storage",
                'size': 100,
                'location': "location",
                'automount': False,
                'format': "xfs"
            }
        )

        bound_volume = response.volume
        action = response.action
        next_actions = response.next_actions

        assert bound_volume._client is volumes_client
        assert bound_volume.id == 4711
        assert bound_volume.name == "database-storage"

        assert action.id == 13
        assert next_actions[0].command == "start_server"

    @pytest.mark.parametrize("server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))])
    def test_create_with_server(self, volumes_client, server, volume_create_response):
        volumes_client._client.request.return_value = volume_create_response
        volumes_client.create(
            100,
            "database-storage",
            server=server,
            automount=False,
            format="xfs"
        )
        volumes_client._client.request.assert_called_with(
            url="/volumes",
            method="POST",
            json={
                'name': "database-storage",
                'size': 100,
                'server': 1,
                'automount': False,
                'format': "xfs"
            }
        )

    def test_create_negative_size(self, volumes_client):
        with pytest.raises(ValueError) as e:
            volumes_client.create(
                -100,
                "database-storage",
                location=Location(name="location")
            )
        assert str(e.value) == "size must be greater than 0"
        volumes_client._client.request.assert_not_called()

    @pytest.mark.parametrize("location,server", [(None, None), ("location", Server(id=1))])
    def test_create_wrong_location_server_combination(self, volumes_client, location, server):
        with pytest.raises(ValueError) as e:
            volumes_client.create(
                100,
                "database-storage",
                location=location,
                server=server
            )
        assert str(e.value) == "only one of server or location must be provided"
        volumes_client._client.request.assert_not_called()

    @pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_get_actions_list(self, volumes_client, volume, response_get_actions):
        volumes_client._client.request.return_value = response_get_actions
        result = volumes_client.get_actions_list(volume, sort="id")
        volumes_client._client.request.assert_called_with(url="/volumes/1/actions", method="GET", params={"sort": "id"})

        actions = result.actions
        assert len(actions) == 1
        assert isinstance(actions[0], BoundAction)

        assert actions[0]._client == volumes_client._client.actions
        assert actions[0].id == 13
        assert actions[0].command == "attach_volume"

    @pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_update(self, volumes_client, volume, response_update_volume):
        volumes_client._client.request.return_value = response_update_volume
        volume = volumes_client.update(volume, name="new-name")
        volumes_client._client.request.assert_called_with(url="/volumes/1", method="PUT", json={"name": "new-name"})

        assert volume.id == 4711
        assert volume.name == "new-name"

    @pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_change_protection(self, volumes_client, volume, generic_action):
        volumes_client._client.request.return_value = generic_action
        action = volumes_client.change_protection(volume, True)
        volumes_client._client.request.assert_called_with(url="/volumes/1/actions/change_protection", method="POST", json={"delete": True})

        assert action.id == 1
        assert action.progress == 0

    @pytest.mark.parametrize("volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_delete(self, volumes_client, volume, generic_action):
        volumes_client._client.request.return_value = generic_action
        delete_success = volumes_client.delete(volume)
        volumes_client._client.request.assert_called_with(url="/volumes/1", method="DELETE")

        assert delete_success is True

    @pytest.mark.parametrize("server,volume",
                             [(Server(id=1), Volume(id=12)),
                              (BoundServer(mock.MagicMock(), dict(id=1)), BoundVolume(mock.MagicMock(), dict(id=12)))])
    def test_attach(self, volumes_client, server, volume, generic_action):
        volumes_client._client.request.return_value = generic_action
        action = volumes_client.attach(volume, server)
        volumes_client._client.request.assert_called_with(
            url="/volumes/12/actions/attach",
            method="POST",
            json={"server": 1}
        )
        assert action.id == 1
        assert action.progress == 0

    @pytest.mark.parametrize("volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))])
    def test_detach(self, volumes_client, volume, generic_action):
        volumes_client._client.request.return_value = generic_action
        action = volumes_client.detach(volume)
        volumes_client._client.request.assert_called_with(
            url="/volumes/12/actions/detach",
            method="POST"
        )
        assert action.id == 1
        assert action.progress == 0

    @pytest.mark.parametrize("volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))])
    def test_resize(self, volumes_client, volume, generic_action):
        volumes_client._client.request.return_value = generic_action
        action = volumes_client.resize(volume, 50)
        volumes_client._client.request.assert_called_with(
            url="/volumes/12/actions/resize",
            method="POST",
            json={"size": 50}
        )
        assert action.id == 1
        assert action.progress == 0
Example #8
0
    def create(self, defn: HcloudDefinition, check, allow_reboot,
               allow_recreate):
        assert isinstance(defn, HcloudDefinition)
        hetzner = defn.config.hcloud
        self.token = get_access_token(hetzner)
        if self.state not in (MachineState.RESCUE, MachineState.UP) or check:
            self.check()

        self.set_common_state(defn)
        self.upgrade_disk = hetzner.upgradeDisk

        # TODO maybe bootstrap can be automated with vncdotool
        image_id = self._fetch_image_id(hetzner.image, hetzner.image_selector)
        if self.image_id is None:
            self.image_id = image_id
        elif self.image_id != image_id:
            self.warn(
                f"image_id changed from {self.image_id} to {image_id} but can't update image of a VM."
            )
        if self.location is None:
            self.location = hetzner.location
        elif self.location != hetzner.location:
            self.warn(
                f"location changed from {self.location} to {hetzner.location} but can't update location of a VM."
            )
        if self.vm_id is not None and hetzner.serverType != self.server_type:
            # TODO Check if server can be upgraded before hitting the Hetzner API
            # https://docs.hetzner.cloud/#server-actions-change-the-type-of-a-server
            do_upgrade = True
            # Only confirm if upgrade_disk is True because then the upgrade can't be undone
            if self.upgrade_disk:
                do_upgrade = self.depl.logger.confirm(
                    f"are you sure you want to change Hetzner server {self.name} type from "
                    + f"{self.server_type} to {hetzner.serverType}?")
            if do_upgrade:
                self.log_start("Changing Hetzner server type...")
                self._server.shutdown().wait_until_finished()
                self.wait_for_down(callback=lambda: self.log_continue("."))
                self._server.change_type(
                    ServerType(name=hetzner.serverType),
                    upgrade_disk=self.upgrade_disk).wait_until_finished()
                self._server.power_on()
                self.wait_for_up(callback=lambda: self.log_continue("."))
                self.log_end("")
        self.server_type = hetzner.serverType

        ssh_keys = [
            k.name if isinstance(k, ResourceEval) else k
            for k in hetzner.sshKeys
        ]
        if self.state != MachineState.MISSING and ssh_keys != self.ssh_keys:
            self.logger.warn(
                f"SSH keys cannot be changed after the server is created.")

        volume_ids = []
        filesystems = {}
        for volumeopts in hetzner.volumes:
            volume = volumeopts.volume
            if isinstance(volume, str):
                volume_model = self._client.volumes.get_by_name(volume)
                volume_name = volume
                volume_id = volume_model.id
                volume_loc = volume_model.location.name
            else:
                volume_res = self.depl.get_typed_resource(
                    volume._name, "hcloud-volume", HcloudVolumeState)
                volume_name = volume_res.name
                volume_id = volume_res.hcloud_id
                assert volume_id is not None
                volume_loc = volume_res.location
            if volume_loc != self.location:
                raise Exception(
                    f"Volume {volume_name!r} is in a different location from server {self.name!r}"
                )
            volume_ids.append(volume_id)
            if volumeopts.mountPoint is not None:
                fs = dict(volumeopts.fileSystem)
                fs["device"] = f"/dev/disk/by-id/scsi-0HC_Volume_{volume_id}"
                filesystems[volumeopts.mountPoint] = fs

        has_priv = self._ssh_private_key is not None
        has_pub = self._ssh_public_key is not None
        assert has_priv == has_pub
        if not has_priv:
            self.log("Generating SSH keypair...")
            (self._ssh_private_key, self._ssh_public_key) = create_key_pair()
        if self.vm_id:
            if self.volume_ids != volume_ids:
                current = set(self.volume_ids)
                new = set(volume_ids)
                volumes_client = self._client.volumes
                self.log_start("Updating volumes...")
                for v in current - new:
                    volumes_client.detach(Volume(id=v))
                    self.log_continue(".")
                for v in new - current:
                    volumes_client.attach(
                        Volume(id=v), self._server,
                        automount=False).wait_until_finished()
                    self.log_continue(".")
                self.log_end("")
                self.volume_ids = volume_ids
        else:
            self.log_start(
                "Creating Hetzner Cloud VM (" +
                f"image '{image_id}', type '{hetzner.serverType}', location '{hetzner.location}'"
                + ")...")
            response = self._client.servers.create(
                name=self.name,
                ssh_keys=[SSHKey(name=k) for k in ssh_keys],
                volumes=[Volume(id=v) for v in volume_ids],
                server_type=ServerType(self.server_type),
                image=Image(id=self.image_id),
                # Set labels so we can find the instance if nixops crashes before writing vm_id
                labels=dict(self._server_labels()),
                user_data=None if self._ssh_public_key is None else yaml.dump(
                    {"public-keys": [self._ssh_public_key]}),
            )
            self.log_end("")
            self.public_ipv4 = response.server.public_net.ipv4.ip
            self.log_start("waiting for SSH...")
            self.wait_for_up(callback=lambda: self.log_continue("."))
            self.log_end("")
            with self.depl._db:
                self.vm_id = response.server.id
                # TODO get state from creation response
                self.state = MachineState.STARTING
                self.ssh_keys = ssh_keys
                self.volume_ids = volume_ids
                self._detect_hardware()
                self._update_host_keys()
        self.filesystems = filesystems
Example #9
0
 def test_created_is_datetime(self):
     server = Volume(id=1, created="2016-01-30T23:50+00:00")
     assert server.created == datetime.datetime(2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0))
Example #10
0
    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")
            ),
            "user_data": self.module.params.get("user_data"),
            "labels": self.module.params.get("labels"),
        }
        image = self.client.images.get_by_name(self.module.params.get("image"))
        if image is not None:
            # When image name is not available look for id instead
            params["image"] = image
        else:
            params["image"] = self.client.images.get_by_id(self.module.params.get("image"))

        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("firewalls") is not None:
            params["firewalls"] = []
            for fw in self.module.params.get("firewalls"):
                f = self.client.firewalls.get_by_name(fw)
                if f is not None:
                    # When firewall name is not available look for id instead
                    params["firewalls"].append(f)
                else:
                    params["firewalls"].append(self.client.firewalls.get_by_id(fw))

        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:
            try:
                resp = self.client.servers.create(**params)
                self.result["root_password"] = resp.root_password
                resp.action.wait_until_finished(max_retries=1000)
                [action.wait_until_finished() for action in resp.next_actions]

                rescue_mode = self.module.params.get("rescue_mode")
                if rescue_mode:
                    self._get_server()
                    self._set_rescue_mode(rescue_mode)

                backups = self.module.params.get("backups")
                if backups:
                    self._get_server()
                    self.hcloud_server.enable_backup().wait_until_finished()

                delete_protection = self.module.params.get("delete_protection")
                rebuild_protection = self.module.params.get("rebuild_protection")
                if delete_protection is not None and rebuild_protection is not None:
                    self._get_server()
                    self.hcloud_server.change_protection(delete=delete_protection,
                                                         rebuild=rebuild_protection).wait_until_finished()
            except Exception as e:
                self.module.fail_json(msg=e.message)
        self._mark_as_changed()
        self._get_server()
Example #11
0
                                  name="Volume2",
                                  location=server2.location)

volume1 = response1.volume
volume2 = response2.volume

# Attach volume to server

client.volumes.attach(server1, volume1)
client.volumes.attach(server2, volume2)

# Detach second volume

client.volumes.detach(volume2)

# Poweroff 2nd server
client.servers.power_off(server2)

# Create one more volume and attach it to server with id=33

server33 = Server(id=33)
response = client.volumes.create(size=33, name="Volume33", server=server33)

print(response.action.status)

# Create one more server and attach 2 volumes to it
client.servers.create("Server3",
                      server_type=ServerType(name="cx11"),
                      image=Image(id=4711),
                      volumes=[Volume(id=221), Volume(id=222)])
Example #12
0
class TestVolumesClient(object):
    def test_get_by_id(self, hetzner_client):
        bound_volume = hetzner_client.volumes.get_by_id(4711)
        assert bound_volume.id == 4711
        assert bound_volume.name == "database-storage"
        assert bound_volume.size == 42

    def test_get_by_name(self, hetzner_client):
        bound_volume = hetzner_client.volumes.get_by_name("database-storage")
        assert bound_volume.id == 4711
        assert bound_volume.name == "database-storage"
        assert bound_volume.size == 42

    def test_get_list(self, hetzner_client):
        result = hetzner_client.volumes.get_list()
        bound_volumes = result.volumes
        assert bound_volumes[0].id == 4711
        assert bound_volumes[0].name == "database-storage"
        assert bound_volumes[0].size == 42

    @pytest.mark.parametrize(
        "server",
        [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))])
    def test_create(self, hetzner_client, server):
        response = hetzner_client.volumes.create(
            42,
            "test-database",
            location=Location(name="nbg1"),
            automount=False,
            format="xfs")

        volume = response.volume
        action = response.action
        next_actions = response.next_actions

        assert volume.id == 4711
        assert volume.name == "database-storage"
        assert volume.size == 42

        assert action.id == 13
        assert action.command == "create_volume"

        assert len(next_actions) == 1
        assert next_actions[0].id == 13
        assert next_actions[0].command == "start_server"

    @pytest.mark.parametrize(
        "volume",
        [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_get_actions(self, hetzner_client, volume):
        actions = hetzner_client.volumes.get_actions(volume)

        assert len(actions) == 1
        assert actions[0].id == 13
        assert actions[0].command == "attach_volume"

    @pytest.mark.parametrize(
        "volume",
        [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_update(self, hetzner_client, volume):
        volume = hetzner_client.volumes.update(volume,
                                               name="new-name",
                                               labels={})

        assert volume.id == 4711
        assert volume.name == "new-name"

    @pytest.mark.parametrize(
        "volume",
        [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))])
    def test_delete(self, hetzner_client, volume):
        delete_success = hetzner_client.volumes.delete(volume)

        assert delete_success is True

    @pytest.mark.parametrize("server,volume",
                             [(Server(id=43), Volume(id=4711)),
                              (BoundServer(mock.MagicMock(), dict(id=43)),
                               BoundVolume(mock.MagicMock(), dict(id=4711)))])
    def test_attach(self, hetzner_client, server, volume):
        action = hetzner_client.volumes.attach(volume, server)
        assert action.id == 13
        assert action.progress == 0
        assert action.command == "attach_volume"

    @pytest.mark.parametrize(
        "volume",
        [Volume(id=4711),
         BoundVolume(mock.MagicMock(), dict(id=4711))])
    def test_detach(self, hetzner_client, volume):
        action = hetzner_client.volumes.detach(volume)
        assert action.id == 13
        assert action.progress == 0
        assert action.command == "detach_volume"

    @pytest.mark.parametrize(
        "volume",
        [Volume(id=4711),
         BoundVolume(mock.MagicMock(), dict(id=4711))])
    def test_resize(self, hetzner_client, volume):
        action = hetzner_client.volumes.resize(volume, 50)
        assert action.id == 13
        assert action.progress == 0
        assert action.command == "resize_volume"