Skip to content
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

Feature/network acls management #546

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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 @@ -42,5 +42,6 @@ These are the contributors to pylxd according to the Github repository.
fliiiix Felix
simondeziel Simon Déziel (Canonical)
sparkiegeek Adam Collard (Canonical)
mobergeron Marc Olivier Bergeron
=============== ==================================

94 changes: 94 additions & 0 deletions doc/source/network-acls.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.. py:currentmodule:: pylxd.models

Network ACLs
========

:class:`NetworkACL` objects show the current network ACLs available to LXD. Creation
and / or modification of network ACLs is possible only if 'network_acl' LXD API
extension is present.


Manager methods
---------------

Network ACLs can be queried through the following client manager
methods:


- :func:`~NetworkACL.all` - Retrieve all networks.
- :func:`~NetworkACL.exists` - See if a network ACL with a name exists.
Returns `bool`.
- :func:`~NetworkACL.get` - Get a specific network ACL, by its name.
- :func:`~NetworkACL.create` - Create a new network ACL.
The name of the network ACL is required. `description`, `egress`, `ingress` and `config`
are optional and the scope of their contents is documented in the LXD
documentation.


Network ACL attributes
------------------

- :attr:`~NetworkACL.name` - The name of the network ACL.
- :attr:`~NetworkACL.description` - The description of the network ACL.
- :attr:`~NetworkACL.egress` - The egress of the network ACL.
- :attr:`~NetworkACL.ingress` - The ingress of the network ACL.
- :attr:`~NetworkACL.used_by` - A list of containers using this network ACL.
- :attr:`~NetworkACL.config` - The configuration associated with the network ACL.


Network ACL methods
---------------

- :func:`~NetworkACL.rename` - Rename the network ACL.
- :func:`~NetworkACL.save` - Save the network ACL. This uses the PUT HTTP method and
not the PATCH.
- :func:`~NetworkACL.delete` - Deletes the network ACL.

.. py:currentmodule:: pylxd.models

Examples
--------

:class:`NetworkACL` operations follow the same manager-style as other
classes. Network ACLs are keyed on a unique name.

.. code-block:: python

>>> client.network_acls.exists('allow-external-ingress')
True

>>> acl = client.network_acls.get('allow-external-ingress')
>>> acl
NetworkACL(config={}, description="Allowing external source for ingress", egress=[], ingress=[{"action": "allow", "description": "Allow external sources", "source": "@external", "state": "enabled"}], name="allow-external-ingress")

>>> print(acl)
{
"name": "allow-external-ingress",
"description": "Allowing external source for ingress",
"egress": [],
"ingress": [
{
"action": "allow",
"source": "@external",
"description": "Allow external sources",
"state": "enabled"
}
],
"config": {},
"used_by": []
}

The network ACL can then be modified and saved.

>>> acl.ingress.append({"action":"allow","state":"enabled"})
>>> acl.save()

Or deleted

>>> acl.delete()

To create a new network ACL, use :func:`~NetworkACL.create` with a name, and optional
arguments: `description` and `egress` and `ingress` and `config`.

>>> acl = client.network_acls.create(name="allow-external-ingress", description="Allowing external source for ingress", ingress=[{"action":"allow","description":"Allow external sources","source":"@external","state":"enabled"}])

42 changes: 42 additions & 0 deletions integration/test_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,45 @@ def test_delete(self):
self.assertRaises(
exceptions.LXDAPIException, self.client.networks.get, self.network.name
)

class TestNetworkACL(NetworkTestCase):
"""Tests for `NetworkACL`."""

def setUp(self):
super().setUp()
name = self.create_network_acl()
self.acl = self.client.network_acls.get(name)

def tearDown(self):
super().tearDown()
self.delete_network_acl(self.acl.name)

def test_save(self):
"""A network ACL is updated"""
self.acl.ingress.insert(0, {"action":"allow","state":"enabled"})
self.acl.save()

acl = self.client.network_acls.get(self.acl.name)
self.assertEqual({"action":"allow","state":"enabled"}, acl.ingress[0])

def test_rename(self):
"""A network ACL is renamed"""
oldName = self.acl.name
name = "allow-new-name"
self.addCleanup(self.delete_network_acl, name)

self.acl.rename(name)
acl = self.client.network_acls.get(name)

self.assertEqual(name, acl.name)
self.assertRaises(
exceptions.LXDAPIException, self.client.network_acls.get, oldName
)

def test_delete(self):
"""A network ACL is deleted"""
self.acl.delete()

self.assertRaises(
exceptions.LXDAPIException, self.client.network_acls.get, self.acl.name
)
16 changes: 16 additions & 0 deletions integration/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,22 @@ def delete_network(self, name):
except exceptions.NotFound:
pass

def create_network_acl(self):
name = self.generate_object_name()
self.lxd.network_acls.post(
json={
"name": name,
"config": {},
}
)
return name

def delete_network_acl(self, name):
try:
self.lxd.network_acls[name].delete()
except exceptions.NotFound:
pass

def assertCommon(self, response):
"""Assert common LXD responses.

Expand Down
3 changes: 2 additions & 1 deletion pylxd/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def __getattr__(self, name):
:rtype: _APINode
"""
# '-' can't be used in variable names
if name in ("storage_pools", "virtual_machines"):
if name in ("storage_pools", "virtual_machines", "network_acls"):
name = name.replace("_", "-")
return self.__class__(
f"{self._api_endpoint}/{name}",
Expand Down Expand Up @@ -435,6 +435,7 @@ def __init__(
self.virtual_machines = managers.VirtualMachineManager(self)
self.images = managers.ImageManager(self)
self.networks = managers.NetworkManager(self)
self.network_acls = managers.NetworkACLManager(self)
self.operations = managers.OperationManager(self)
self.profiles = managers.ProfileManager(self)
self.projects = managers.ProjectManager(self)
Expand Down
4 changes: 4 additions & 0 deletions pylxd/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class NetworkForwardManager(BaseManager):
manager_for = "pylxd.models.NetworkForward"


class NetworkACLManager(BaseManager):
manager_for = "pylxd.models.NetworkACL"


class OperationManager(BaseManager):
manager_for = "pylxd.models.Operation"

Expand Down
3 changes: 2 additions & 1 deletion pylxd/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pylxd.models.container import Container
from pylxd.models.image import Image
from pylxd.models.instance import Instance, Snapshot
from pylxd.models.network import Network, NetworkForward
from pylxd.models.network import Network, NetworkACL, NetworkForward
from pylxd.models.operation import Operation
from pylxd.models.profile import Profile
from pylxd.models.project import Project
Expand All @@ -19,6 +19,7 @@
"Image",
"Instance",
"Network",
"NetworkACL",
"NetworkForward",
"Operation",
"Profile",
Expand Down
143 changes: 143 additions & 0 deletions pylxd/models/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,149 @@ def __repr__(self):
return f"{self.__class__.__name__}({', '.join(sorted(attrs))})"


class NetworkACL(model.Model):
"""Model representing a LXD network ACL."""

name = model.Attribute()
description = model.Attribute()
egress = model.Attribute()
ingress = model.Attribute()
config = model.Attribute()
used_by = model.Attribute(readonly=True)
_endpoint = "network-acls"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

@classmethod
def exists(cls, client, name):
"""
Determine whether network ACL with provided name exists.

:param client: client instance
:type client: :class:`~pylxd.client.Client`
:param name: name of the network ACL
:type name: str
:returns: `True` if network ACL exists, `False` otherwise
:rtype: bool
"""
try:
client.network_acls.get(name)
return True
except cls.NotFound:
return False

@classmethod
def get(cls, client, name):
"""
Get a network ACL by name.

:param client: client instance
:type client: :class:`~pylxd.client.Client`
:param name: name of the network ACL
:type name: str
:returns: network ACL instance (if exists)
:rtype: :class:`NetworkACL`
:raises: :class:`~pylxd.exceptions.NotFound` if network ACL does not exist
"""
response = client.api.network_acls[name].get()

return cls(client, **response.json()["metadata"])

@classmethod
def all(cls, client):
"""
Get all network ACLs.

:param client: client instance
:type client: :class:`~pylxd.client.Client`
:rtype: list[:class:`NetworkACL`]
"""
response = client.api.network_acls.get()

acls = []
for url in response.json()["metadata"]:
name = url.split("/")[-1]
acls.append(cls(client, name=name))
return acls

@classmethod
def create(
cls, client, name, description=None, egress=None, ingress=None, config=None
):
"""
Create a network ACL.

:param client: client instance
:type client: :class:`~pylxd.client.Client`
:param name: name of the network ACL
:type name: str
:param description: description of the network ACL
:type description: str
:param egress: egress of the network ACL
:type egress: list
:param ingress: ingress of the network ACL
:type ingress: list
:param config: additional configuration
:type config: dict
"""
client.assert_has_api_extension("network_acl")

acl = {"name": name}
if description is not None:
acl["description"] = description
if egress is not None:
acl["egress"] = egress
if ingress is not None:
acl["ingress"] = ingress
if config is not None:
acl["config"] = config
client.api.network_acls.post(json=acl)
return cls.get(client, name)

def rename(self, new_name):
"""
Rename a network ACL.

:param new_name: new name of the network ACL
:type new_name: str
:return: Renamed network ACL instance
:rtype: :class:`NetworkACL`
"""
self.client.assert_has_api_extension("network_acl")
self.client.api.network_acls[self.name].post(json={"name": new_name})
return NetworkACL.get(self.client, new_name)

def save(self, *args, **kwargs):
self.client.assert_has_api_extension("network_acl")
super().save(*args, **kwargs)

def log(self):
"""Get network acl log."""
response = self.api.log.get()
pring(response.json()["metadata"])
log = NetworkACLLog(response.json()["metadata"])
return log

@property
def api(self):
return self.client.api.network_acls[self.name]

def __str__(self):
return json.dumps(self.marshall(skip_readonly=False), indent=2)

def __repr__(self):
attrs = []
for attribute, value in self.marshall().items():
attrs.append(f"{attribute}={json.dumps(value, sort_keys=True)}")

return f"{self.__class__.__name__}({', '.join(sorted(attrs))})"


class NetworkACLLog(model.AttributeDict):
"""A simple object for representing a network state."""


class NetworkState(model.AttributeDict):
"""A simple object for representing a network state."""

Expand Down
Loading