-
Notifications
You must be signed in to change notification settings - Fork 129
feat: support port ranges and endpoints #2366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9037c7e
e5c6f72
b589903
dabc9b8
6a5c1ad
c3a23a6
d1db204
8e19250
8eff659
6890fdc
9adbc6a
736281c
0a1c7d5
ea70537
5740058
eca96c6
a4f7864
33e29e7
f0615c2
db71dbb
39ea347
de8ced4
e2c36cf
e4a8ab1
2440682
e167b25
d211adf
054b2d3
f8f9319
bbaa6a5
14943cc
13d1c19
3627956
271657c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,7 @@ | |
| import warnings | ||
| import weakref | ||
| from abc import ABC, abstractmethod | ||
| from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping | ||
| from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping, Sequence | ||
| from pathlib import Path, PurePath | ||
| from typing import ( | ||
| Any, | ||
|
|
@@ -719,7 +719,11 @@ def add_secret( | |
| ) | ||
|
|
||
| def open_port( | ||
| self, protocol: typing.Literal['tcp', 'udp', 'icmp'], port: int | None = None | ||
| self, | ||
| protocol: typing.Literal['tcp', 'udp', 'icmp'], | ||
| port: int | tuple[int, int | None] | None = None, | ||
| *, | ||
| endpoints: Sequence[str] = '*', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be better / more Pythonic for this input parameter, so |
||
| ) -> None: | ||
| """Open a port with the given protocol for this unit. | ||
|
|
||
|
|
@@ -736,18 +740,30 @@ def open_port( | |
| protocol: String representing the protocol; must be one of | ||
| 'tcp', 'udp', or 'icmp' (lowercase is recommended, but | ||
| uppercase is also supported). | ||
| port: The port to open. Required for TCP and UDP; not allowed | ||
| for ICMP. | ||
| port: The port to open. Required for TCP and UDP; not allowed for ICMP. | ||
| May be a tuple of two integers to specify a port range. | ||
| endpoints: The endpoints for which to open the port. | ||
| '*' means to open the port for all endpoints. | ||
|
|
||
| Raises: | ||
| ModelError: If ``port`` is provided when ``protocol`` is 'icmp' | ||
| or ``port`` is not provided when ``protocol`` is 'tcp' or | ||
| 'udp'. | ||
| """ | ||
| self._backend.open_port(protocol.lower(), port) | ||
| if isinstance(port, tuple): | ||
| port, to_port = port | ||
| else: | ||
| port, to_port = port, None | ||
| if not endpoints: | ||
| raise TypeError('endpoints must be a non-empty string or sequence of strings') | ||
| self._backend.open_port(protocol.lower(), port, to_port=to_port, endpoints=endpoints) | ||
|
|
||
| def close_port( | ||
| self, protocol: typing.Literal['tcp', 'udp', 'icmp'], port: int | None = None | ||
| self, | ||
| protocol: typing.Literal['tcp', 'udp', 'icmp'], | ||
| port: int | tuple[int, int | None] | None = None, | ||
| *, | ||
| endpoints: Sequence[str] = '*', | ||
| ) -> None: | ||
| """Close a port with the given protocol for this unit. | ||
|
|
||
|
|
@@ -765,21 +781,29 @@ def close_port( | |
| protocol: String representing the protocol; must be one of | ||
| 'tcp', 'udp', or 'icmp' (lowercase is recommended, but | ||
| uppercase is also supported). | ||
| port: The port to open. Required for TCP and UDP; not allowed | ||
| for ICMP. | ||
| port: The port to open. Required for TCP and UDP; not allowed for ICMP. | ||
| May be a tuple of two integers to specify a port range. | ||
| endpoints: The endpoints for which to close the port. | ||
| '*' means to close the port for all endpoints. | ||
|
|
||
| Raises: | ||
| ModelError: If ``port`` is provided when ``protocol`` is 'icmp' | ||
| or ``port`` is not provided when ``protocol`` is 'tcp' or | ||
| 'udp'. | ||
| """ | ||
| self._backend.close_port(protocol.lower(), port) | ||
| if isinstance(port, tuple): | ||
| port, to_port = port | ||
| else: | ||
| port, to_port = port, None | ||
| if not endpoints: | ||
| raise TypeError('endpoints must be a non-empty string or sequence of strings') | ||
| self._backend.close_port(protocol.lower(), port, to_port=to_port, endpoints=endpoints) | ||
|
|
||
| def opened_ports(self) -> set[Port]: | ||
| """Return a list of opened ports for this unit.""" | ||
| return self._backend.opened_ports() | ||
|
|
||
| def set_ports(self, *ports: int | Port) -> None: | ||
| def set_ports(self, *ports: int | tuple[int, int | None] | Port) -> None: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per discussion, I'd be inclined to drop this change, and they can pass in |
||
| """Set the open ports for this unit, closing any others that are open. | ||
|
|
||
| Some behaviour, such as whether the port is opened or closed externally without | ||
|
|
@@ -800,16 +824,19 @@ def set_ports(self, *ports: int | Port) -> None: | |
| ``port`` is not ``None``, or where ``protocol`` is 'tcp' or 'udp' and ``port`` | ||
| is ``None``. | ||
| """ | ||
| # Normalise to get easier comparisons. | ||
| existing = {(port.protocol, port.port) for port in self._backend.opened_ports()} | ||
| existing = self._backend.opened_ports() | ||
| desired = { | ||
| ('tcp', port) if isinstance(port, int) else (port.protocol, port.port) | ||
| Port('tcp', port) | ||
| if isinstance(port, int) | ||
| else Port('tcp', port[0], to_port=port[1]) | ||
| if isinstance(port, tuple) | ||
| else port | ||
| for port in ports | ||
| } | ||
| for protocol, port in existing - desired: | ||
| self._backend.close_port(protocol, port) | ||
| for protocol, port in desired - existing: | ||
| self._backend.open_port(protocol, port) | ||
| for p in existing - desired: | ||
| self._backend.close_port(p.protocol, p.port, to_port=p.to_port, endpoints=p.endpoints) | ||
| for p in desired - existing: | ||
| self._backend.open_port(p.protocol, p.port, to_port=p.to_port, endpoints=p.endpoints) | ||
|
|
||
| def reboot(self, now: bool = False) -> None: | ||
| """Reboot the host machine. | ||
|
|
@@ -846,6 +873,21 @@ class Port: | |
| port: int | None | ||
| """The port number. Will be ``None`` if protocol is ``'icmp'``.""" | ||
|
|
||
| to_port: int | None = None | ||
| """The end of the port range, if a range was specified. | ||
|
|
||
| Will be ``None`` if a single port was specified (or if protocol is ``'icmp'``). | ||
| """ | ||
|
|
||
| _: dataclasses.KW_ONLY | ||
|
|
||
| endpoints: tuple[str, ...] | Literal['*'] = '*' | ||
| """The endpoints for which the port is open. | ||
|
|
||
| Will be ``"*"`` if open for all endpoints, | ||
| or a tuple of endpoint names if specified for particular endpoints. | ||
| """ | ||
|
|
||
|
|
||
| OpenedPort = Port | ||
| """Alias to Port for backwards compatibility. | ||
|
|
@@ -4037,26 +4079,59 @@ def secret_remove(self, id: str, *, revision: int | None = None): | |
| with self._wrap_hookcmd('secret-remove', id=id, revision=revision): | ||
| hookcmds.secret_remove(id, revision=revision) | ||
|
|
||
| def open_port(self, protocol: str, port: int | None = None): | ||
| with self._wrap_hookcmd('open-port', protocol=protocol, port=port): | ||
| hookcmds.open_port(protocol, port) | ||
| def open_port( | ||
| self, | ||
| protocol: str, | ||
| port: int | None = None, | ||
| *, | ||
| to_port: int | None = None, | ||
| endpoints: Sequence[str] = '*', | ||
| ): | ||
| with self._wrap_hookcmd( | ||
| 'open-port', protocol=protocol, port=port, to_port=to_port, endpoints=endpoints | ||
| ): | ||
| result = hookcmds.open_port(protocol, port, to_port=to_port, endpoints=endpoints) | ||
| if result is not None: | ||
| raise ModelError(result) | ||
|
|
||
| def close_port(self, protocol: str, port: int | None = None): | ||
| with self._wrap_hookcmd('close-port', protocol=protocol, port=port): | ||
| hookcmds.close_port(protocol, port) | ||
| def close_port( | ||
| self, | ||
| protocol: str, | ||
| port: int | None = None, | ||
| *, | ||
| to_port: int | None = None, | ||
| endpoints: Sequence[str] = '*', | ||
| ): | ||
| with self._wrap_hookcmd( | ||
| 'close-port', protocol=protocol, port=port, to_port=to_port, endpoints=endpoints | ||
| ): | ||
| result = hookcmds.close_port(protocol, port, to_port=to_port, endpoints=endpoints) | ||
| if result is not None: | ||
| raise ModelError(result) | ||
|
|
||
| def opened_ports(self) -> set[Port]: | ||
| with self._wrap_hookcmd('opened-ports'): | ||
| results = hookcmds.opened_ports() | ||
| with self._wrap_hookcmd('opened-ports', endpoints=True): | ||
| result = hookcmds.opened_ports(endpoints=True) | ||
| ports: set[Port] = set() | ||
| for raw_port in results: | ||
| if raw_port.protocol not in ('tcp', 'udp', 'icmp'): | ||
| logger.warning('Unexpected opened-ports protocol: %s', raw_port.protocol) | ||
| for port in result: | ||
| if port.protocol not in ('tcp', 'udp', 'icmp'): | ||
| logger.warning('Unexpected opened-ports protocol: %s', port.protocol) | ||
| continue | ||
| if not port.endpoints: | ||
| logger.warning('opened-ports result with no endpoints: %s', port) | ||
| continue | ||
| if raw_port.to_port is not None: | ||
| logger.warning('Ignoring opened-ports port range: %s', raw_port) | ||
| port = Port(raw_port.protocol or 'tcp', raw_port.port) | ||
| ports.add(port) | ||
| match port.endpoints: | ||
| case ['*']: | ||
| model_endpoints = '*' | ||
| case _: | ||
| model_endpoints = tuple(port.endpoints) | ||
| model_port = Port( | ||
| port.protocol, | ||
| port.port, | ||
| to_port=port.to_port, | ||
| endpoints=model_endpoints, | ||
| ) | ||
| ports.add(model_port) | ||
| return ports | ||
|
|
||
| def reboot(self, now: bool = False): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a bug in Juju, or at least a wart. I don't think we should return stdout from hookcmds, but detect the error (from stdout) and raise an exception here. We should also open a bug on Juju to return a non-zero exit code for this case.