From dc0cc29a3a93580f3f614c4ce4b8d4f368cf76f8 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 18:21:33 +0800 Subject: [PATCH 1/6] 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 2/6] 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 fb9a638626230cd9e106d1f1f2b5007d430a7805 Mon Sep 17 00:00:00 2001 From: charlie4284 Date: Sat, 27 Jan 2024 19:11:06 +0800 Subject: [PATCH 3/6] 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 4/6] 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 5/6] 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 6/6] 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