From dc0cc29a3a93580f3f614c4ce4b8d4f368cf76f8 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 18:21:33 +0800 Subject: [PATCH 01/11] chore: migrate statuses --- juju/status.py | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/juju/status.py b/juju/status.py index 46485dac6..e3e5e4b83 100644 --- a/juju/status.py +++ b/juju/status.py @@ -2,6 +2,7 @@ # Licensed under the Apache V2, see LICENCE file for details. import logging +from typing import Literal from .client import client @@ -14,6 +15,196 @@ """ +# Status values common to machine and unit agents. +CommonStatusT = Literal['error', 'started'] + +# Error means the entity requires human intervention +# in order to operate correctly. +ERROR: CommonStatusT = 'error' + +# Started is set when: +# The entity is actively participating in the model. +# For unit agents, this is a state we preserve for backwards +# compatibility with scripts during the life of Juju 1.x. +# In Juju 2.x, the agent-state will remain “active” and scripts +# will watch the unit-state instead for signals of application readiness. +STARTED: CommonStatusT = 'started' + + +# Status values specific to machine agents. +MachineAgentStatusT = Literal['pending', 'stopped', 'down'] | CommonStatusT + +# Pending is set when: +# The machine is not yet participating in the model. +PENDING: MachineAgentStatusT = 'pending' + +# Stopped is set when: +# The machine's agent will perform no further action, other than +# to set the unit to Dead at a suitable moment. +STOPPED: MachineAgentStatusT = 'stopped' + +# Down is set when: +# The machine ought to be signalling activity, but it cannot be +# detected. +DOWN: MachineAgentStatusT = 'down' + + +# Status values specific to unit agents. +UnitAgentStatusT = ( + Literal['allocating', 'rebooting', 'executing', 'idle', 'failed', 'lost'] + | CommonStatusT +) + +# Allocating is set when: +# The machine on which a unit is to be hosted is still being +# spun up in the cloud. +ALLOCATING: UnitAgentStatusT = 'allocating' + +# Rebooting is set when: +# The machine on which this agent is running is being rebooted. +# The juju-agent should move from rebooting to idle when the reboot is complete. +REBOOTING: UnitAgentStatusT = 'rebooting' + +# Executing is set when: +# The agent is running a hook or action. The human-readable message should reflect +# which hook or action is being run. +EXECUTING: UnitAgentStatusT = 'executing' + +# Idle is set when: +# Once the agent is installed and running it will notify the Juju server and its state +# becomes 'idle'. It will stay 'idle' until some action (e.g. it needs to run a hook) or +# error (e.g it loses contact with the Juju server) moves it to a different state. +IDLE: UnitAgentStatusT = 'idle' + +# Failed is set when: +# The unit agent has failed in some way,eg the agent ought to be signalling +# activity, but it cannot be detected. It might also be that the unit agent +# detected an unrecoverable condition and managed to tell the Juju server about it. +FAILED: UnitAgentStatusT = 'failed' + +# Lost is set when: +# The juju agent has not communicated with the juju server for an unexpectedly long time; +# the unit agent ought to be signalling activity, but none has been detected. +LOST: UnitAgentStatusT = 'lost' + +# Status values specific to applications and units, reflecting the +# state of the software itself. +AppOrUnitStatusT = Literal[ + 'unset', 'maintenance', 'terminated', 'unknown', 'waiting', 'blocked', 'active' +] +# Unset is only for applications, and is a placeholder status. +# The core/cache package deals with aggregating the unit status +# to the application level. +UNSET: AppOrUnitStatusT = 'unset' + +# Maintenance is set when: +# The unit is not yet providing services, but is actively doing stuff +# in preparation for providing those services. +# This is a 'spinning' state, not an error state. +# It reflects activity on the unit itself, not on peers or related units. +MAINTENANCE: AppOrUnitStatusT = 'maintenance' + +# Terminated is set when: +# This unit used to exist, we have a record of it (perhaps because of storage +# allocated for it that was flagged to survive it). Nonetheless, it is now gone. +TERMINATED: AppOrUnitStatusT = 'terminated' + +# Unknown is set when: +# A unit-agent has finished calling install, config-changed, and start, +# but the charm has not called : AppOrUnitStatusT-set yet. +UNKNOWN: AppOrUnitStatusT = 'unknown' + +# Waiting is set when: +# The unit is unable to progress to an active state because an application to +# which it is related is not running. +WAITING: AppOrUnitStatusT = 'waiting' + +# Blocked is set when: +# The unit needs manual intervention to get back to the Running state. +BLOCKED: AppOrUnitStatusT = 'blocked' + +# Active is set when: +# The unit believes it is correctly offering all the services it has +# been asked to offer. +ACTIVE: AppOrUnitStatusT = 'active' + +# Status values specific to storage. +StorageStatusT = Literal['attaching', 'attached', 'detaching', 'detached'] + +# Attaching indicates that the storage is being attached +# to a machine. +ATTACHING: StorageStatusT = 'attaching' + +# Attached indicates that the storage is attached to a +# machine. +ATTACHED: StorageStatusT = 'attached' + +# Detaching indicates that the storage is being detached +# from a machine. +DETACHING: StorageStatusT = 'detaching' + +# Detached indicates that the storage is not attached to +# any machine. +DETACHED: StorageStatusT = 'detached' + +# Status values specific to models. +ModelStatusT = Literal['available', 'busy'] + +# Available indicates that the model is available for use. +AVAILABLE: ModelStatusT = 'available' + +# Busy indicates that the model is not available for use because it is +# running a process that must take the model offline, such as a migration, +# upgrade, or backup. This is a spinning state, it is not an error state, +# and it should be expected that the model will eventually go back to +# available. +BUSY: ModelStatusT = 'busy' + +# Status values specific to relations. +RelationStatusT = Literal['joining', 'joined', 'broken', 'suspending'] + +# Joining is used to signify that a relation should become joined soon. +JOINING: RelationStatusT = 'joining' + +# Joined is the normal : RelationStatusT for a healthy, alive relation. +JOINED: RelationStatusT = 'joined' + +# Broken is the : RelationStatusT for when a relation life goes to Dead. +BROKEN: RelationStatusT = 'broken' + +# Suspending is used to signify that a relation will be temporarily broken +# pending action to resume it. +SUSPENDING: RelationStatusT = 'suspending' + +# Suspended is used to signify that a relation is temporarily broken pending +# action to resume it. +SUSPENDED: RelationStatusT = 'suspended' + +# Status values that are common to several entities. +CommonEntityStatusT = Literal['destroying'] + +# Destroying indicates that the entity is being destroyed. +# This is valid for volumes, filesystems, and models. +DESTROYING: CommonEntityStatusT = 'destroying' + +# InstanceStatus +InstanceStatusT = Literal['', 'allocating', 'running', 'provisioning error'] +EMPTY: InstanceStatusT = '' +PROVISIONING: InstanceStatusT = 'allocating' +RUNNING: InstanceStatusT = 'running' +PROVISIONING_ERROR: InstanceStatusT = 'provisioning error' + +# ModificationStatus +ModificationStatusT = Literal['applied'] +APPLIED: ModificationStatusT = 'applied' + +# Messages +MESSAGE_WAIT_FOR_MACHINE = 'waiting for machine' +MESSAGE_WAIT_FOR_CONTAINER = 'waiting for container' +MESSAGE_INSTALLING_AGENT = 'installing agent' +MESSAGE_INITIALIZING_AGENT = 'agent initialising' +MESSAGE_INSTALLING_CHARM = 'installing charm software' + def derive_status(statues): current = 'unknown' From 8f5b2251a20963ecb9c76267cc2bf012a6a07fb1 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 18:22:55 +0800 Subject: [PATCH 02/11] chore: add type hints to machines --- juju/machine.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index 60554fd09..ec0528065 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -6,11 +6,13 @@ import pyrfc3339 -from . import model, tag, jasyncio +from juju.utils import juju_ssh_key_paths + +from . import jasyncio, model, tag from .annotationhelper import _get_annotations, _set_annotations from .client import client from .errors import JujuError -from juju.utils import juju_ssh_key_paths +from .status import InstanceStatusT, MachineAgentStatusT log = logging.getLogger(__name__) @@ -37,14 +39,14 @@ async def destroy(self, force=False): return await self.model._wait('machine', self.id, 'remove') remove = destroy - async def get_annotations(self): + async def get_annotations(self) -> dict[str,str]: """Get annotations on this machine. :return dict: The annotations for this application """ return await _get_annotations(self.tag, self.connection) - async def set_annotations(self, annotations): + async def set_annotations(self, annotations: dict[str,str]): """Set annotations on this machine. :param annotations map[string]string: the annotations as key/value @@ -53,7 +55,7 @@ async def set_annotations(self, annotations): """ return await _set_annotations(self.tag, annotations, self.connection) - def _format_addr(self, addr): + def _format_addr(self, addr: str): """Validate and format IP address. :param addr: IPv6 or IPv4 address @@ -69,8 +71,8 @@ def _format_addr(self, addr): fmt = '{}' return fmt.format(ipaddr) - async def scp_to(self, source, destination, user='ubuntu', proxy=False, - scp_opts=''): + async def scp_to(self, source: str, destination: str, user:str ='ubuntu', proxy: bool=False, + scp_opts: str | list[str]=''): """Transfer files to this machine. :param str source: Local path of file(s) to transfer @@ -92,8 +94,8 @@ async def scp_to(self, source, destination, user='ubuntu', proxy=False, destination = '{}@{}:{}'.format(user, address, destination) await self._scp(source, destination, scp_opts) - async def scp_from(self, source, destination, user='ubuntu', proxy=False, - scp_opts=''): + async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', + proxy: bool = False, scp_opts: str | list[str] = ''): """Transfer files from this machine. :param str source: Remote path of file(s) to transfer @@ -115,7 +117,7 @@ async def scp_from(self, source, destination, user='ubuntu', proxy=False, source = '{}@{}:{}'.format(user, address, source) await self._scp(source, destination, scp_opts) - async def _scp(self, source, destination, scp_opts): + async def _scp(self, source: str, destination: str, scp_opts: str | list[str]): """ Execute an scp command. Requires a fully qualified source and destination. """ @@ -135,7 +137,8 @@ async def _scp(self, source, destination, scp_opts): raise JujuError("command failed: %s" % cmd) async def ssh( - self, command, user='ubuntu', proxy=False, ssh_opts=None): + self, command: str, user: str = 'ubuntu', proxy: bool = False, + ssh_opts: str | list[str] = None): """Execute a command over SSH on this machine. :param str command: Command to execute @@ -168,7 +171,7 @@ async def ssh( return stdout.decode() @property - def agent_status(self): + def agent_status(self) -> MachineAgentStatusT: """Returns the current Juju agent status string. """ @@ -182,7 +185,7 @@ def agent_status_since(self): return pyrfc3339.parse(self.safe_data['agent-status']['since']) @property - def agent_version(self): + def agent_version(self) -> str: """Get the version of the Juju machine agent. May return None if the agent is not yet available. @@ -194,7 +197,7 @@ def agent_version(self): return None @property - def status(self): + def status(self) -> InstanceStatusT: """Returns the current machine provisioning status string. """ @@ -215,7 +218,7 @@ def status_since(self): return pyrfc3339.parse(self.safe_data['instance-status']['since']) @property - def dns_name(self): + def dns_name(self) -> str | None: """Get the DNS name for this machine. This is a best guess based on the addresses available in current data. @@ -236,7 +239,7 @@ def dns_name(self): return None @property - def hostname(self): + def hostname(self) -> str | None: """Get the hostname for this machine as reported by the machine agent running on it. This is only supported on 2.8.10+ controllers. From a771e7c639903654b27746d2d32d64c77cbdea99 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 18:57:25 +0800 Subject: [PATCH 03/11] fix: wait for machine address to be ready --- juju/machine.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index ec0528065..b0d1986d2 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -6,7 +6,7 @@ import pyrfc3339 -from juju.utils import juju_ssh_key_paths +from juju.utils import juju_ssh_key_paths, block_until from . import jasyncio, model, tag from .annotationhelper import _get_annotations, _set_annotations @@ -84,7 +84,8 @@ async def scp_to(self, source: str, destination: str, user:str ='ubuntu', proxy: """ if proxy: raise NotImplementedError('proxy option is not implemented') - + if not self.dns_name: + raise JujuError("Machine address not yet ready, please call await machine.wait()") try: # if dns_name is an IP address format it appropriately address = self._format_addr(self.dns_name) @@ -107,7 +108,8 @@ async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', """ if proxy: raise NotImplementedError('proxy option is not implemented') - + if not self.dns_name: + raise JujuError("Machine address not yet ready, please call await machine.wait()") try: # if dns_name is an IP address format it appropriately address = self._format_addr(self.dns_name) @@ -150,6 +152,8 @@ async def ssh( if proxy: raise NotImplementedError('proxy option is not implemented') address = self.dns_name + if not address: + raise JujuError("Machine address not yet ready, please call await machine.wait()") destination = "{}@{}".format(user, address) _, id_path = juju_ssh_key_paths() cmd = [ @@ -170,6 +174,14 @@ async def ssh( # stdout is a bytes-like object, returning a string might be more useful return stdout.decode() + async def wait(self, timeout: int=300) -> None: + """Waits until the machie is ready to take ssh/scp commands. + + :param int timeout: Timeout in seconds to wait for. + """ + await block_until(lambda: self.safe_data["addresses"] and + self.agent_status == "started", timeout=timeout) + @property def agent_status(self) -> MachineAgentStatusT: """Returns the current Juju agent status string. From 0459ee7a3e3bdef176dd6bc621a3272c67ac5302 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:07:52 +0800 Subject: [PATCH 04/11] fix: assertion --- tests/integration/test_machine.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/integration/test_machine.py b/tests/integration/test_machine.py index f0f82c188..032f0bb49 100644 --- a/tests/integration/test_machine.py +++ b/tests/integration/test_machine.py @@ -6,6 +6,7 @@ import pytest from .. import base +from juju.machine import Machine @base.bootstrapped @@ -36,3 +37,11 @@ async def test_status(): machine.status_message.lower() == 'running' and machine.agent_status == 'started')), timeout=480) + +async def test_machine_ssh(): + async with base.CleanModel() as model: + machine: Machine = await model.add_machine() + await machine.wait() + out = await machine.ssh("echo hello world!") + + assert out == "hello world!\n" From fb9a638626230cd9e106d1f1f2b5007d430a7805 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:11:06 +0800 Subject: [PATCH 05/11] fix: typing --- juju/machine.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index ec0528065..df26a5c88 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -3,7 +3,7 @@ import ipaddress import logging - +import typing import pyrfc3339 from juju.utils import juju_ssh_key_paths @@ -72,7 +72,7 @@ def _format_addr(self, addr: str): return fmt.format(ipaddr) async def scp_to(self, source: str, destination: str, user:str ='ubuntu', proxy: bool=False, - scp_opts: str | list[str]=''): + scp_opts: typing.Union[str, typing.List[str]] =''): """Transfer files to this machine. :param str source: Local path of file(s) to transfer @@ -95,7 +95,7 @@ async def scp_to(self, source: str, destination: str, user:str ='ubuntu', proxy: await self._scp(source, destination, scp_opts) async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', - proxy: bool = False, scp_opts: str | list[str] = ''): + proxy: bool = False, scp_opts: typing.Union[str, typing.List[str]] = ''): """Transfer files from this machine. :param str source: Remote path of file(s) to transfer @@ -117,7 +117,7 @@ async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', source = '{}@{}:{}'.format(user, address, source) await self._scp(source, destination, scp_opts) - async def _scp(self, source: str, destination: str, scp_opts: str | list[str]): + async def _scp(self, source: str, destination: str, scp_opts: typing.Union[str, typing.List[str]]): """ Execute an scp command. Requires a fully qualified source and destination. """ @@ -138,7 +138,7 @@ async def _scp(self, source: str, destination: str, scp_opts: str | list[str]): async def ssh( self, command: str, user: str = 'ubuntu', proxy: bool = False, - ssh_opts: str | list[str] = None): + ssh_opts: typing.Optional[typing.Union[str, typing.List[str]]] = None): """Execute a command over SSH on this machine. :param str command: Command to execute @@ -218,7 +218,7 @@ def status_since(self): return pyrfc3339.parse(self.safe_data['instance-status']['since']) @property - def dns_name(self) -> str | None: + def dns_name(self) -> typing.Optional[str]: """Get the DNS name for this machine. This is a best guess based on the addresses available in current data. @@ -239,7 +239,7 @@ def dns_name(self) -> str | None: return None @property - def hostname(self) -> str | None: + def hostname(self) -> typing.Optional[str]: """Get the hostname for this machine as reported by the machine agent running on it. This is only supported on 2.8.10+ controllers. From 7f1a5aa26e5d9b109622e4a64ec577192964cff7 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:13:41 +0800 Subject: [PATCH 06/11] fix: typing dict --- juju/machine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index df26a5c88..62e939695 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -39,14 +39,14 @@ async def destroy(self, force=False): return await self.model._wait('machine', self.id, 'remove') remove = destroy - async def get_annotations(self) -> dict[str,str]: + async def get_annotations(self) -> typing.Dict[str,str]: """Get annotations on this machine. :return dict: The annotations for this application """ return await _get_annotations(self.tag, self.connection) - async def set_annotations(self, annotations: dict[str,str]): + async def set_annotations(self, annotations: typing.Dict[str,str]): """Set annotations on this machine. :param annotations map[string]string: the annotations as key/value From 186bbd0a9f6703aa96d99beaa1433948762b4b0b Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:18:55 +0800 Subject: [PATCH 07/11] fix: typing union --- juju/status.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/juju/status.py b/juju/status.py index e3e5e4b83..1c1d2019a 100644 --- a/juju/status.py +++ b/juju/status.py @@ -32,7 +32,7 @@ # Status values specific to machine agents. -MachineAgentStatusT = Literal['pending', 'stopped', 'down'] | CommonStatusT +MachineAgentStatusT = Union[Literal['pending', 'stopped', 'down'], CommonStatusT] # Pending is set when: # The machine is not yet participating in the model. @@ -50,10 +50,10 @@ # Status values specific to unit agents. -UnitAgentStatusT = ( - Literal['allocating', 'rebooting', 'executing', 'idle', 'failed', 'lost'] - | CommonStatusT -) +UnitAgentStatusT = Union[ + Literal['allocating', 'rebooting', 'executing', 'idle', 'failed', 'lost'], + CommonStatusT, +] # Allocating is set when: # The machine on which a unit is to be hosted is still being From 1ec83013954f94c7c43f121117a4d604470932a2 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:20:46 +0800 Subject: [PATCH 08/11] fix: lint --- juju/machine.py | 8 ++++---- juju/status.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index 62e939695..db217bfd2 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -39,14 +39,14 @@ async def destroy(self, force=False): return await self.model._wait('machine', self.id, 'remove') remove = destroy - async def get_annotations(self) -> typing.Dict[str,str]: + async def get_annotations(self) -> typing.Dict[str, str]: """Get annotations on this machine. :return dict: The annotations for this application """ return await _get_annotations(self.tag, self.connection) - async def set_annotations(self, annotations: typing.Dict[str,str]): + async def set_annotations(self, annotations: typing.Dict[str, str]): """Set annotations on this machine. :param annotations map[string]string: the annotations as key/value @@ -71,8 +71,8 @@ def _format_addr(self, addr: str): fmt = '{}' return fmt.format(ipaddr) - async def scp_to(self, source: str, destination: str, user:str ='ubuntu', proxy: bool=False, - scp_opts: typing.Union[str, typing.List[str]] =''): + async def scp_to(self, source: str, destination: str, user: str = 'ubuntu', proxy: bool = False, + scp_opts: typing.Union[str, typing.List[str]] = ''): """Transfer files to this machine. :param str source: Local path of file(s) to transfer diff --git a/juju/status.py b/juju/status.py index 1c1d2019a..a74d65578 100644 --- a/juju/status.py +++ b/juju/status.py @@ -2,7 +2,7 @@ # Licensed under the Apache V2, see LICENCE file for details. import logging -from typing import Literal +from typing import Literal, Union from .client import client From 13b5fc001a3915c3b818223244df0d4ad15477e5 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:21:49 +0800 Subject: [PATCH 09/11] fix: lint --- juju/machine.py | 9 +++++---- tests/integration/test_machine.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index 971062e9d..5610c8dfe 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -4,9 +4,10 @@ import ipaddress import logging import typing + import pyrfc3339 -from juju.utils import juju_ssh_key_paths, block_until +from juju.utils import block_until, juju_ssh_key_paths from . import jasyncio, model, tag from .annotationhelper import _get_annotations, _set_annotations @@ -174,12 +175,12 @@ async def ssh( # stdout is a bytes-like object, returning a string might be more useful return stdout.decode() - async def wait(self, timeout: int=300) -> None: + async def wait(self, timeout: int = 300) -> None: """Waits until the machie is ready to take ssh/scp commands. - + :param int timeout: Timeout in seconds to wait for. """ - await block_until(lambda: self.safe_data["addresses"] and + await block_until(lambda: self.safe_data["addresses"] and self.agent_status == "started", timeout=timeout) @property diff --git a/tests/integration/test_machine.py b/tests/integration/test_machine.py index 032f0bb49..ce92445ce 100644 --- a/tests/integration/test_machine.py +++ b/tests/integration/test_machine.py @@ -38,6 +38,7 @@ async def test_status(): machine.agent_status == 'started')), timeout=480) + async def test_machine_ssh(): async with base.CleanModel() as model: machine: Machine = await model.add_machine() From 756bcbecec8eb46b24285a1e792f2f322f4ffedf Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:29:05 +0800 Subject: [PATCH 10/11] fix: typo in docstring --- juju/machine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index 5610c8dfe..d890ed28d 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -176,9 +176,9 @@ async def ssh( return stdout.decode() async def wait(self, timeout: int = 300) -> None: - """Waits until the machie is ready to take ssh/scp commands. + """Waits until the machine is ready to take ssh/scp commands. - :param int timeout: Timeout in seconds to wait for. + :param int timeout: Timeout in seconds. """ await block_until(lambda: self.safe_data["addresses"] and self.agent_status == "started", timeout=timeout) From 17f461de56b2ebbfe9611cc078dd168e08815231 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Wed, 31 Jan 2024 18:02:25 +0800 Subject: [PATCH 11/11] fix: use self.addresses --- juju/machine.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/juju/machine.py b/juju/machine.py index d890ed28d..c77e8fa4d 100644 --- a/juju/machine.py +++ b/juju/machine.py @@ -73,7 +73,8 @@ def _format_addr(self, addr: str): return fmt.format(ipaddr) async def scp_to(self, source: str, destination: str, user: str = 'ubuntu', proxy: bool = False, - scp_opts: typing.Union[str, typing.List[str]] = ''): + scp_opts: typing.Union[str, typing.List[str]] = '', + wait_for_active: bool = True, timeout: typing.Optional[int] = None): """Transfer files to this machine. :param str source: Local path of file(s) to transfer @@ -82,11 +83,13 @@ async def scp_to(self, source: str, destination: str, user: str = 'ubuntu', prox :param bool proxy: Proxy through the Juju API server :param scp_opts: Additional options to the `scp` command :type scp_opts: str or list + :param bool wait_for_active: Wait until the machine is ready to take in ssh commands. + :param int timeout: Time in seconds to wait until the machine becomes ready. """ if proxy: raise NotImplementedError('proxy option is not implemented') - if not self.dns_name: - raise JujuError("Machine address not yet ready, please call await machine.wait()") + if wait_for_active: + await block_until(lambda: self.addresses, timeout=timeout) try: # if dns_name is an IP address format it appropriately address = self._format_addr(self.dns_name) @@ -97,7 +100,8 @@ async def scp_to(self, source: str, destination: str, user: str = 'ubuntu', prox await self._scp(source, destination, scp_opts) async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', - proxy: bool = False, scp_opts: typing.Union[str, typing.List[str]] = ''): + proxy: bool = False, scp_opts: typing.Union[str, typing.List[str]] = '', + wait_for_active: bool = True, timeout: typing.Optional[int] = None): """Transfer files from this machine. :param str source: Remote path of file(s) to transfer @@ -106,11 +110,13 @@ async def scp_from(self, source: str, destination: str, user: str = 'ubuntu', :param bool proxy: Proxy through the Juju API server :param scp_opts: Additional options to the `scp` command :type scp_opts: str or list + :param bool wait_for_active: Wait until the machine is ready to take in ssh commands. + :param int timeout: Time in seconds to wait until the machine becomes ready. """ if proxy: raise NotImplementedError('proxy option is not implemented') - if not self.dns_name: - raise JujuError("Machine address not yet ready, please call await machine.wait()") + if wait_for_active: + await block_until(lambda: self.addresses, timeout=timeout) try: # if dns_name is an IP address format it appropriately address = self._format_addr(self.dns_name) @@ -141,20 +147,22 @@ async def _scp(self, source: str, destination: str, scp_opts: typing.Union[str, async def ssh( self, command: str, user: str = 'ubuntu', proxy: bool = False, - ssh_opts: typing.Optional[typing.Union[str, typing.List[str]]] = None): + ssh_opts: typing.Optional[typing.Union[str, typing.List[str]]] = None, + wait_for_active: bool = True, timeout: typing.Optional[int] = None): """Execute a command over SSH on this machine. :param str command: Command to execute :param str user: Remote username :param bool proxy: Proxy through the Juju API server :param str ssh_opts: Additional options to the `ssh` command - + :param bool wait_for_active: Wait until the machine is ready to take in ssh commands. + :param int timeout: Time in seconds to wait until the machine becomes ready. """ if proxy: raise NotImplementedError('proxy option is not implemented') address = self.dns_name - if not address: - raise JujuError("Machine address not yet ready, please call await machine.wait()") + if wait_for_active: + await block_until(lambda: self.addresses, timeout=timeout) destination = "{}@{}".format(user, address) _, id_path = juju_ssh_key_paths() cmd = [ @@ -175,13 +183,14 @@ async def ssh( # stdout is a bytes-like object, returning a string might be more useful return stdout.decode() - async def wait(self, timeout: int = 300) -> None: - """Waits until the machine is ready to take ssh/scp commands. - - :param int timeout: Timeout in seconds. + + @property + def addresses(self) -> typing.List[str]: + """Returns the machine addresses. + """ - await block_until(lambda: self.safe_data["addresses"] and - self.agent_status == "started", timeout=timeout) + return self.safe_data['addresses'] or [] + @property def agent_status(self) -> MachineAgentStatusT: @@ -237,11 +246,10 @@ def dns_name(self) -> typing.Optional[str]: May return None if no suitable address is found. """ - addresses = self.safe_data['addresses'] or [] ordered_addresses = [] ordered_scopes = ['public', 'local-cloud', 'local-fan'] for scope in ordered_scopes: - for address in addresses: + for address in self.addresses: if scope == address['scope']: ordered_addresses.append(address) for address in ordered_addresses: