Skip to content

feat: list all vm instances#903

Open
Aeonoi wants to merge 7 commits intocanonical:mainfrom
Aeonoi:issue-639-list-instances
Open

feat: list all vm instances#903
Aeonoi wants to merge 7 commits intocanonical:mainfrom
Aeonoi:issue-639-list-instances

Conversation

@Aeonoi
Copy link

@Aeonoi Aeonoi commented Feb 6, 2026

  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • Have you successfully run make lint && make test?

Closes #639


@Aeonoi Aeonoi marked this pull request as ready for review February 10, 2026 21:30
@Aeonoi Aeonoi requested a review from a team as a code owner February 10, 2026 21:30
Copilot AI review requested due to automatic review settings February 10, 2026 21:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a provider-level API for enumerating existing provider instances (to support cleanup workflows requested in #639), with initial implementations for LXD and Multipass and a new LXD unit test.

Changes:

  • Introduce Provider.list_instances() as a new abstract method.
  • Implement list_instances() for LXDProvider and MultipassProvider.
  • Add __repr__ methods for LXDInstance and MultipassInstance, plus a unit test for LXD listing.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/unit/lxd/test_lxd_provider.py Adds a unit test covering LXDProvider.list_instances().
craft_providers/provider.py Adds the new abstract list_instances() API to the provider base class.
craft_providers/multipass/multipass_provider.py Implements MultipassProvider.list_instances().
craft_providers/multipass/multipass_instance.py Adds MultipassInstance.__repr__.
craft_providers/lxd/lxd_provider.py Implements LXDProvider.list_instances().
craft_providers/lxd/lxd_instance.py Adds LXDInstance.__repr__.
Comments suppressed due to low confidence (1)

craft_providers/provider.py:42

  • Provider now has duplicate TYPE_CHECKING imports / blocks in this area, which makes the import section confusing and can lead to inconsistent type-only imports. Please consolidate into a single from typing import TYPE_CHECKING and a single if TYPE_CHECKING: block with the needed annotation-only imports.
from typing import TYPE_CHECKING

from .base import Base

if TYPE_CHECKING:
    from collections.abc import Callable, Collection

    from craft_providers import Executor
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import pathlib

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 98 to 100
def list_instances(self) -> Collection[Executor]:
"""Get a collection of existing instances for this provider."""

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new list_instances() abstract method has no way to scope results to the current application or to control inclusion of LXD base instances (the issue calls out “base containers…behind a flag” and multipass filtering by relevant name). Consider extending the API to accept scoping inputs (e.g. project_name/instance_name_prefix) plus an include_base_instances: bool = False flag so providers can return only relevant instances by default.

Suggested change
def list_instances(self) -> Collection[Executor]:
"""Get a collection of existing instances for this provider."""
def list_instances(
self,
*,
project_name: str | None = None,
instance_name_prefix: str | None = None,
include_base_instances: bool = False,
) -> Collection[Executor]:
"""Get a collection of existing instances for this provider.
:param project_name: Optional project name to scope instances to a specific
application or project, if supported by the provider.
:param instance_name_prefix: Optional prefix to filter instances by name.
:param include_base_instances: If True, include any base instances created
by the provider; if False (default), base instances should be excluded
from the results when possible.
"""

Copilot uses AI. Check for mistakes.
Comment on lines 105 to 112
def list_instances(self) -> Collection[LXDInstance]:
"""Get a collection of all existing instances for this LXD provider."""
return [
LXDInstance(name=name, project=self.lxd_project, remote=self.lxd_remote)
for name in self.lxc.list_names(
project=self.lxd_project, remote=self.lxd_remote
)
]
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LXDProvider.list_instances() constructs LXDInstance objects without passing the provider’s existing self.lxc (and without a shared pylxd client), so each instance will create its own LXC() wrapper (and its own pylxd.Client). This adds unnecessary overhead when listing many instances; consider passing lxc=self.lxc and reusing a single client instance across the returned executors.

Copilot uses AI. Check for mistakes.
Comment on lines 105 to 112
def list_instances(self) -> Collection[LXDInstance]:
"""Get a collection of all existing instances for this LXD provider."""
return [
LXDInstance(name=name, project=self.lxd_project, remote=self.lxd_remote)
for name in self.lxc.list_names(
project=self.lxd_project, remote=self.lxd_remote
)
]
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #639 specifies that LXD “base containers [should be] behind a flag”, but this implementation returns all instances in the project, including base instances created by craft-providers. Consider excluding base instances by default (e.g. filter names starting with base-instance-) and adding an explicit opt-in flag to include them.

Copilot uses AI. Check for mistakes.
self._multipass = Multipass()

def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, instance_name={self.instance_name})"
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultipassInstance.__repr__ currently formats instance_name without !r, so the repr output is ambiguous for names containing special characters and is inconsistent with the name field. Use instance_name={self.instance_name!r} for a proper, unambiguous repr.

Suggested change
return f"{self.__class__.__name__}(name={self.name!r}, instance_name={self.instance_name})"
return f"{self.__class__.__name__}(name={self.name!r}, instance_name={self.instance_name!r})"

Copilot uses AI. Check for mistakes.
Comment on lines 107 to 108
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, instance_name={self.instance_name!r}, default_command_environment={self.default_command_environment!r}, project={self.project!r}, remote={self.remote!r})"
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LXDInstance.__repr__ includes default_command_environment, which can be large and tends to make debug logs/noise harder to read when instances are logged. Consider limiting the repr to stable identifiers (e.g. instance_name, project, remote) and omitting large mutable fields.

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +187
def list_instances(self) -> Collection[MultipassInstance]:
"""Get a collection of all existing multipass VMs."""
return [
MultipassInstance(name=name, multipass=self.multipass)
for name in self.multipass.list()
]
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue #639 calls for listing only provider instances relevant to the current application (for multipass: VMs beginning with a relevant name), but MultipassProvider.list_instances() currently wraps and returns all VMs from multipass list. This can surface unrelated VMs and make cleanup dangerous; consider adding a prefix/project scoping mechanism and filtering to only those instances by default.

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +187
def list_instances(self) -> Collection[MultipassInstance]:
"""Get a collection of all existing multipass VMs."""
return [
MultipassInstance(name=name, multipass=self.multipass)
for name in self.multipass.list()
]
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultipassProvider.list_instances() introduces new behavior but there’s no unit test coverage for it (and tests exist for other MultipassProvider methods). Please add a test in tests/unit/multipass/test_multipass_provider.py that stubs multipass.list() and asserts the returned instances (and any filtering semantics, once added).

Copilot uses AI. Check for mistakes.
Comment on lines +315 to +324
def test_lxd_list_instances(mock_lxc_container):
"""Verify LXDProvider's instances"""
provider = LXDProvider(
lxc=mock_lxc_container,
lxd_project="default",
lxd_remote="local",
)

instances = list(provider.list_instances())

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test_lxd_list_instances verifies returned instance names but doesn’t assert that lxc.list_names was called with the provider’s lxd_project/lxd_remote. Adding an assertion on the mock call would better lock in the intended behavior.

Copilot uses AI. Check for mistakes.
@Aeonoi Aeonoi changed the title Issue 639 list instances feat: list all vm instances Feb 10, 2026
@steinbro steinbro requested review from bepri and lengau February 11, 2026 02:51
Copy link
Member

@bepri bepri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work so far :). Copilot's comments seem reasonable to me, could you incorporate those?


self._client = client or pylxd.Client(project=self.project)

def __repr__(self) -> str:
Copy link
Member

@bepri bepri Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was just left behind for debugging, can you remove it now?

Also, as a tip, Python has a nice shorthand syntax for the var={var!r} pattern:

print(f"{var=}")

else:
self._multipass = Multipass()

def __repr__(self) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide a way to list (relevant) provider instances

2 participants