Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ These are the contributors to pylxd according to the Github repository.
simondeziel Simon Déziel (Canonical)
sparkiegeek Adam Collard (Canonical)
mate-amargo Juan Alberto Regalado Galvan
gajeshbhat Gajesh Bhat
=============== ==================================

43 changes: 40 additions & 3 deletions doc/source/instances.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ Instances can be queried through the following client manager
methods:

- `exists(name)` - Returns `boolean` indicating if the instance exists.
- `all()` - Retrieve all instances.
- `all(recursion=0, fields=None)` - Retrieve all instances.
- `recursion` controls how much data is pre-fetched: ``0`` returns names
only, ``1`` returns basic attributes, ``2`` returns full state including
network and disk information.
- `fields` is an optional list of state sub-fields to fetch selectively
(e.g. ``["state.disk"]``, ``["state.disk", "state.network"]``). Only
meaningful with ``recursion=2`` and requires the
``instances_state_selective_recursion`` LXD API extension. When the
extension is absent the parameter is silently ignored and the full state
is returned. Pass an empty list (``fields=[]``) to suppress all
expensive state fields.
- `get()` - Get a specific instance, by its name.
- `create(config, wait=False, target='lxd-cluster-member')` - Create a new instance.
- `create(config, wait=False, target='lxd-cluster-member')` - Create a new instance.
- This method requires the instance config as the first parameter.
- The config itself is beyond the scope of this documentation. Please refer to the LXD documentation for more information.
- The config itself is beyond the scope of this documentation. Please refer to the LXD documentation for more information.
- This method will also return immediately, unless `wait` is `True`.
- Optionally, the target node can be specified for LXD clusters.

Expand Down Expand Up @@ -102,6 +112,33 @@ get a list of all LXD instances with `all`.
[<instance.Instance at 0x7f95d8af72b0>,]
To pre-fetch full instance data in a single request, use ``recursion=2``.

.. code-block:: python
>>> instances = client.instances.all(recursion=2)
If the LXD server supports the ``instances_state_selective_recursion``
extension, you can request only specific state sub-fields to reduce data
transfer. This is useful when, for example, you only need disk usage and
want to avoid the cost of enumerating network interfaces.

.. code-block:: python
# Only fetch disk state
>>> instances = client.instances.all(recursion=2, fields=["state.disk"])
# Fetch both disk and network state
>>> instances = client.instances.all(recursion=2, fields=["state.disk", "state.network"])
# Suppress all expensive state fields entirely
>>> instances = client.instances.all(recursion=2, fields=[])
If the extension is not available the call falls back gracefully to a
standard ``recursion=2`` request.


In order to create a new :class:`~instance.Instance`, an instance
config dictionary is needed, containing a name and the source. A create
operation is asynchronous, so the operation will take some time. If you'd
Expand Down
125 changes: 125 additions & 0 deletions integration/test_instances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright (c) 2016 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from integration.testing import IntegrationTestCase


class TestInstances(IntegrationTestCase):
"""Tests for `Client.instances`."""

def test_get(self):
"""An instance is fetched by name."""
name = self.create_container()
self.addCleanup(self.delete_container, name)

instance = self.client.instances.get(name)

self.assertEqual(name, instance.name)

def test_all(self):
"""A list of all instances is returned."""
name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all()

self.assertIn(name, [i.name for i in instances])

def test_all_recursion_1(self):
"""A list of instances with basic attributes is returned."""
name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all(recursion=1)

self.assertIn(name, [i.name for i in instances])

def test_all_recursion_2(self):
"""A list of instances with full state is returned at recursion=2."""
name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all(recursion=2)

self.assertIn(name, [i.name for i in instances])

def test_all_selective_recursion_disk(self):
"""Selective recursion with state.disk returns instances correctly.
Requires the ``instances_state_selective_recursion`` LXD API
extension. The test is skipped if the extension is not present.
"""
if not self.client.has_api_extension("instances_state_selective_recursion"):
self.skipTest("instances_state_selective_recursion extension not available")

name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all(recursion=2, fields=["state.disk"])

self.assertIn(name, [i.name for i in instances])

def test_all_selective_recursion_multiple_fields(self):
"""Selective recursion with multiple fields returns instances correctly.
Requires the ``instances_state_selective_recursion`` LXD API
extension. The test is skipped if the extension is not present.
"""
if not self.client.has_api_extension("instances_state_selective_recursion"):
self.skipTest("instances_state_selective_recursion extension not available")

name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all(
recursion=2, fields=["state.disk", "state.network"]
)

self.assertIn(name, [i.name for i in instances])

def test_all_selective_recursion_empty_fields(self):
"""Selective recursion with empty fields list suppresses all state.
Requires the ``instances_state_selective_recursion`` LXD API
extension. The test is skipped if the extension is not present.
"""
if not self.client.has_api_extension("instances_state_selective_recursion"):
self.skipTest("instances_state_selective_recursion extension not available")

name = self.create_container()
self.addCleanup(self.delete_container, name)

instances = self.client.instances.all(recursion=2, fields=[])

self.assertIn(name, [i.name for i in instances])

def test_all_selective_recursion_fallback(self):
"""When the extension is absent, fields is ignored and all() succeeds.
This verifies the graceful-fallback path: passing ``fields`` on a
server that does not advertise the extension must not raise and must
still return the full instance list.
"""
if self.client.has_api_extension("instances_state_selective_recursion"):
self.skipTest(
"Server has the extension; fallback path cannot be tested here"
)

name = self.create_container()
self.addCleanup(self.delete_container, name)

# Should fall back to plain recursion=2 without raising.
instances = self.client.instances.all(recursion=2, fields=["state.disk"])

self.assertIn(name, [i.name for i in instances])
35 changes: 33 additions & 2 deletions pylxd/models/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,18 +305,49 @@ def get(cls, client, name):
return cls(client, **response.json()["metadata"])

@classmethod
def all(cls, client, recursion=0):
def all(cls, client, recursion=0, fields=None):
"""Get all instances.

This method returns an Instance array. If recursion is unset,
only the name of each instance will be set and `Instance.sync`
can be used to return more information. If recursion is between
1-2 this method will pre-fetch additional instance attributes for
all instances in the array.

If `fields` is provided and the server supports the
``instances_state_selective_recursion`` extension, only the
specified state sub-fields are fetched (for example:
``state.disk``, ``state.network``; additional values may be
supported by LXD). Pass an empty list to suppress all
expensive state fields. ``fields`` is only meaningful when
``recursion=2``; it is silently ignored otherwise, or when the
extension is unavailable (in which case the server returns all
state fields as usual).

:param recursion: Recursion level (0, 1, or 2).
:type recursion: int
:param fields: Selective state fields to fetch (requires
``instances_state_selective_recursion`` extension).
:type fields: list[str] or None
"""
params = {}
if recursion != 0:
params = {"recursion": recursion}
if (
recursion == 2
and fields is not None
and client.has_api_extension("instances_state_selective_recursion")
):
if isinstance(fields, (str, bytes)):
raise TypeError(
"fields must be an iterable of strings, not str or bytes"
)
for field in fields:
if not isinstance(field, str):
raise TypeError("fields must be an iterable of strings")
fields_str = ",".join(fields)
params = {"recursion": f"2;fields={fields_str}"}
else:
params = {"recursion": recursion}
response = client.api[cls._endpoint].get(params=params)

instances = []
Expand Down
Loading