From 6822d838f470fe6e88fca5cd09e6f2db696cfd0d Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Thu, 3 Oct 2019 13:26:29 -0400 Subject: [PATCH 01/29] Release 0.6.5. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c7b42c..660fcfd 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(filename): author='MAAS Developers', author_email='maas-devel@lists.launchpad.net', url='https://github.com/maas/python-libmaas', - version="0.6.4", + version="0.6.5", classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', From 069e75885aaaf17964d5820e32554c39d0e9c7ac Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Fri, 28 Feb 2020 10:37:50 +0000 Subject: [PATCH 02/29] Fix machine details for a machine with tags (#236) Viscera layer produces a Tags.Managed object for machines with tags, this can't be str()ified. --- maas/client/bones/testing/server.py | 8 +-- maas/client/flesh/tables.py | 10 +++- maas/client/flesh/tests/test_machines.py | 66 +++++++++++++++++++++++- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/maas/client/bones/testing/server.py b/maas/client/bones/testing/server.py index 9fd8d63..15648e8 100644 --- a/maas/client/bones/testing/server.py +++ b/maas/client/bones/testing/server.py @@ -22,9 +22,11 @@ def __init__(self, description): super(ApplicationBuilder, self).__init__() self._description = desc.Description(description) self._application = aiohttp.web.Application() - self._rootpath, self._basepath, self._version = ( - self._discover_version_and_paths() - ) + ( + self._rootpath, + self._basepath, + self._version, + ) = self._discover_version_and_paths() self._wire_up_description() self._actions = {} self._views = {} diff --git a/maas/client/flesh/tables.py b/maas/client/flesh/tables.py index 53acf3f..7004759 100644 --- a/maas/client/flesh/tables.py +++ b/maas/client/flesh/tables.py @@ -155,6 +155,14 @@ def render(self, target, data): return super().render(target, data.name) +class NodeTagsColumn(Column): + def render(self, target, data): + if data: + return super().render(target, [tag.name for tag in data]) + else: + return "" + + class NodesTable(Table): def __init__(self): super().__init__( @@ -215,7 +223,7 @@ def __init__(self, with_type=False): NodeResourcePoolColumn("pool", "Resource pool"), NodeZoneColumn("zone", "Zone"), NodeOwnerColumn("owner", "Owner"), - Column("tags", "Tags"), + NodeTagsColumn("tags", "Tags"), ] if with_type: columns.insert(1, NodeTypeColumn("node_type", "Type")) diff --git a/maas/client/flesh/tests/test_machines.py b/maas/client/flesh/tests/test_machines.py index 22f946d..6e3c941 100644 --- a/maas/client/flesh/tests/test_machines.py +++ b/maas/client/flesh/tests/test_machines.py @@ -1,5 +1,6 @@ """Tests for `maas.client.flesh.machines`.""" +from functools import partial from operator import itemgetter import yaml @@ -10,13 +11,14 @@ from ...viscera.testing import bind from ...viscera.machines import Machine, Machines from ...viscera.resource_pools import ResourcePool +from ...viscera.tags import Tag, Tags from ...viscera.users import User from ...viscera.zones import Zone def make_origin(): """Make origin for machines.""" - return bind(Machines, Machine, User, ResourcePool, Zone) + return bind(Machines, Machine, User, ResourcePool, Zone, Tag, Tags) class TestMachines(TestCaseWithProfile): @@ -55,7 +57,7 @@ def test_returns_table_with_machines(self): cmd = machines.cmd_machines(parser) subparser = machines.cmd_machines.register(parser) options = subparser.parse_args([]) - output = yaml.load( + output = yaml.safe_load( cmd.execute(origin, options, target=tabular.RenderTarget.yaml) ) self.assertEquals( @@ -101,3 +103,63 @@ def test_calls_handler_with_hostnames(self): options = subparser.parse_args(hostnames) cmd.execute(origin, options, target=tabular.RenderTarget.yaml) origin.Machines._handler.read.assert_called_once_with(hostname=hostnames) + + +class TestMachine(TestCaseWithProfile): + """Tests for `cmd_machine`.""" + + def setUp(self): + super().setUp() + origin = make_origin() + parser = ArgumentParser() + self.hostname = make_name_without_spaces() + machine_objs = [ + { + "hostname": self.hostname, + "architecture": "amd64/generic", + "status": NodeStatus.READY.value, + "status_name": NodeStatus.READY.name, + "owner": None, + "power_state": PowerState.OFF.value, + "cpu_count": 2, + "memory": 1024, + "pool": {"id": 1, "name": "pool1", "description": "pool1"}, + "zone": {"id": 1, "name": "zone1", "description": "zone1"}, + "tag_names": ["tag1", "tag2"], + "distro_series": "", + "power_type": "Manual", + }, + ] + origin.Machines._handler.read.return_value = machine_objs + cmd = machines.cmd_machine(parser) + subparser = machines.cmd_machine.register(parser) + options = subparser.parse_args([machine_objs[0]["hostname"]]) + self.cmd = partial(cmd.execute, origin, options) + + def test_yaml_machine_details_with_tags(self): + yaml_output = yaml.safe_load(self.cmd(target=tabular.RenderTarget.yaml)) + self.assertEqual(yaml_output.get("tags"), ["tag1", "tag2"]) + + def test_plain_machine_details_with_tags(self): + plain_output = self.cmd(target=tabular.RenderTarget.plain) + self.assertEqual( + plain_output, + f"""\ ++---------------+-------------+ +| Hostname | {self.hostname} | +| Status | READY | +| Image | (none) | +| Power | Off | +| Power Type | Manual | +| Arch | amd64 | +| #CPUs | 2 | +| RAM | 1.0 GB | +| Interfaces | 0 physical | +| IP addresses | | +| Resource pool | pool1 | +| Zone | zone1 | +| Owner | (none) | +| Tags | tag1 | +| | tag2 | ++---------------+-------------+""", + ) From bc1d76ae5fbb550924d63ec2655759d1eb130ca3 Mon Sep 17 00:00:00 2001 From: Jason Hobbs Date: Thu, 8 Oct 2020 06:06:05 -0500 Subject: [PATCH 03/29] Iterate over a copy of items in ActionAPI.__call__. (#247) This fixes issue #246. Previously, this buggy code in python 3, due to it modifying keys of a dictionary being iterated over. --- maas/client/bones/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maas/client/bones/__init__.py b/maas/client/bones/__init__.py index 92ce5ad..28f26cf 100644 --- a/maas/client/bones/__init__.py +++ b/maas/client/bones/__init__.py @@ -301,7 +301,7 @@ async def __call__(self, **data): del data[key] for nested_key, nested_value in value.items(): data[key + "_" + nested_key] = nested_value - for key, value in data.items(): + for key, value in data.copy().items(): if key.startswith("_"): data[key[1:]] = data.pop(key) response = await self.bind(**params).call(**data) From 575d3f762afc69ae91349aa75a893a53826d6f22 Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Fri, 9 Oct 2020 16:45:24 +0100 Subject: [PATCH 04/29] Release 0.6.6 Signed-off-by: Adam Collard --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 660fcfd..97e5dcd 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(filename): author='MAAS Developers', author_email='maas-devel@lists.launchpad.net', url='https://github.com/maas/python-libmaas', - version="0.6.5", + version="0.6.6", classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', From 94fe03efec5470b1192dcabed51b811e74801b78 Mon Sep 17 00:00:00 2001 From: Alberto Donato Date: Thu, 10 Dec 2020 11:11:44 +0100 Subject: [PATCH 05/29] switch to Github actions (#250) --- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 17 ------------- README | 6 ++--- 3 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3cee93 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Repository checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.6" + + - name: Install dependencies + run: | + pip install --upgrade pip tox + + - name: Lint + run: | + tox -e lint + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.6" + - "3.7" + - "3.8" + steps: + - name: Repository checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install --upgrade pip tox codecov + + - name: Test + run: | + tox -e py + codecov diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 25f3edb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -python: - - "3.6" - - "3.7" - -install: - - pip install codecov tox - -script: - - tox -e py3,lint - -after_success: - - codecov --env TRAVIS_PYTHON_VERSION - -branches: - only: - - master diff --git a/README b/README index 1361fae..fb3fce3 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ Python client API library made especially for [MAAS][]. -[![Build Status](https://travis-ci.org/maas/python-libmaas.svg?branch=master)](https://travis-ci.org/maas/python-libmaas) +[![CI tests](https://github.com/maas/python-libmaas/workflows/CI%20tests/badge.svg)](https://github.com/maas/python-libmaas/actions?query=workflow%3A%22CI+tests%22) [![codecov.io](https://codecov.io/github/maas/python-libmaas/coverage.svg?branch=master)](https://codecov.io/github/maas/python-libmaas?branch=master) @@ -11,9 +11,9 @@ Python client API library made especially for [MAAS][]. All the dependencies are declared in `setup.py` so this can be installed with [pip](https://pip.pypa.io/). Python 3.5+ is required. -When working from trunk it can be helpful to use `virtualenv`: +When working from master it can be helpful to use a virtualenv: - $ virtualenv --python=python3 amc && source amc/bin/activate + $ python3 -m venv ve && source ve/bin/activate $ pip install git+https://github.com/maas/python-libmaas.git $ maas --help From 1f6378049e8f3cc8a43941ae0e4a02b5f8afc3c7 Mon Sep 17 00:00:00 2001 From: Diego Mascialino Date: Fri, 19 Feb 2021 12:06:40 -0300 Subject: [PATCH 06/29] Update index.md (#254) Modify import line in order to fix: ``` NameError: name 'maas' is not defined ``` --- doc/client/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/client/index.md b/doc/client/index.md index c379b78..d6f6e9a 100644 --- a/doc/client/index.md +++ b/doc/client/index.md @@ -11,7 +11,7 @@ Web API. ```python #!/usr/bin/env python3.6 -from maas.client import connect +import maas.client # Replace … with an API key previously obtained by hand from # http://$host:$port/MAAS/account/prefs/. From ae3e321924b231ae5b765cd70cdd074cc2a9929e Mon Sep 17 00:00:00 2001 From: mkinney Date: Wed, 12 Jan 2022 03:06:10 -0800 Subject: [PATCH 07/29] fix black warnings; update for yaml issue; bump version; add 3.9 to ci (#268) * fix black warnings; update for yaml issue; add 3.9 to ci * match the versions in setup.py with the ci --- .github/workflows/ci.yml | 1 + maas/client/errors.py | 6 +++--- maas/client/flesh/tests/test_controllers.py | 2 +- maas/client/flesh/tests/test_devices.py | 2 +- maas/client/flesh/tests/test_nodes.py | 2 +- maas/client/viscera/tests/test_sshkeys.py | 6 +++--- setup.py | 4 +++- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3cee93..847e4ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: - "3.6" - "3.7" - "3.8" + - "3.9" steps: - name: Repository checkout uses: actions/checkout@v2 diff --git a/maas/client/errors.py b/maas/client/errors.py index bf2396e..963339f 100644 --- a/maas/client/errors.py +++ b/maas/client/errors.py @@ -10,15 +10,15 @@ def __init__(self, msg, obj): class OperationNotAllowed(Exception): - """ MAAS says this operation cannot be performed. """ + """MAAS says this operation cannot be performed.""" class ObjectNotLoaded(Exception): - """ Object is not loaded. """ + """Object is not loaded.""" class CannotDelete(Exception): - """ Object cannot be deleted. """ + """Object cannot be deleted.""" class PowerError(MAASException): diff --git a/maas/client/flesh/tests/test_controllers.py b/maas/client/flesh/tests/test_controllers.py index c57a808..e5e7e41 100644 --- a/maas/client/flesh/tests/test_controllers.py +++ b/maas/client/flesh/tests/test_controllers.py @@ -70,7 +70,7 @@ def test_returns_table_with_controllers(self): cmd = controllers.cmd_controllers(parser) subparser = controllers.cmd_controllers.register(parser) options = subparser.parse_args([]) - output = yaml.load( + output = yaml.safe_load( cmd.execute(origin, options, target=tabular.RenderTarget.yaml) ) self.assertEquals( diff --git a/maas/client/flesh/tests/test_devices.py b/maas/client/flesh/tests/test_devices.py index 1c0e710..2b35572 100644 --- a/maas/client/flesh/tests/test_devices.py +++ b/maas/client/flesh/tests/test_devices.py @@ -49,7 +49,7 @@ def test_returns_table_with_devices(self): cmd = devices.cmd_devices(parser) subparser = devices.cmd_devices.register(parser) options = subparser.parse_args([]) - output = yaml.load( + output = yaml.safe_load( cmd.execute(origin, options, target=tabular.RenderTarget.yaml) ) self.assertEquals( diff --git a/maas/client/flesh/tests/test_nodes.py b/maas/client/flesh/tests/test_nodes.py index 76ade87..5dce260 100644 --- a/maas/client/flesh/tests/test_nodes.py +++ b/maas/client/flesh/tests/test_nodes.py @@ -48,7 +48,7 @@ def test_returns_table_with_nodes(self): cmd = nodes.cmd_nodes(parser) subparser = nodes.cmd_nodes.register(parser) options = subparser.parse_args([]) - output = yaml.load( + output = yaml.safe_load( cmd.execute(origin, options, target=tabular.RenderTarget.yaml) ) self.assertEquals( diff --git a/maas/client/viscera/tests/test_sshkeys.py b/maas/client/viscera/tests/test_sshkeys.py index ce570e0..3220ee7 100644 --- a/maas/client/viscera/tests/test_sshkeys.py +++ b/maas/client/viscera/tests/test_sshkeys.py @@ -17,7 +17,7 @@ def make_origin(): class TestSSHKeys(TestCase): def test__sshkeys_create(self): - """ SSHKeys.create() returns a new SSHKey. """ + """SSHKeys.create() returns a new SSHKey.""" SSHKeys = make_origin().SSHKeys key = make_string_without_spaces() SSHKeys._handler.create.return_value = {"id": 1, "key": key, "keysource": ""} @@ -25,7 +25,7 @@ def test__sshkeys_create(self): SSHKeys._handler.create.assert_called_once_with(key=key) def test__sshkeys_read(self): - """ SSHKeys.read() returns a list of SSH keys. """ + """SSHKeys.read() returns a list of SSH keys.""" SSHKeys = make_origin().SSHKeys keys = [ { @@ -42,7 +42,7 @@ def test__sshkeys_read(self): class TestSSHKey(TestCase): def test__sshkey_read(self): - """ SSHKeys.read() returns a single SSH key. """ + """SSHKeys.read() returns a single SSH key.""" SSHKey = make_origin().SSHKey key_id = random.randint(0, 100) key_dict = {"id": key_id, "key": make_string_without_spaces(), "keysource": ""} diff --git a/setup.py b/setup.py index 97e5dcd..5b642c7 100644 --- a/setup.py +++ b/setup.py @@ -34,8 +34,10 @@ def read(filename): 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU Affero General Public License v3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries', ], namespace_packages=['maas'], From fc44f195e5c15ccea5dcb99ad1854f527df59426 Mon Sep 17 00:00:00 2001 From: Milad Alkhamis Date: Mon, 11 Apr 2022 21:33:23 +0430 Subject: [PATCH 08/29] Fix Python 3.10 Collections package import error (#271) * Fix Python 3.10 Collections package import error * Fix Iterable,Mapping import * Fix python 3.10 import error * Fix python3.10 import --- maas/client/bones/__init__.py | 4 +++- maas/client/utils/__init__.py | 6 ++++-- maas/client/utils/multipart.py | 2 +- maas/client/viscera/__init__.py | 3 ++- maas/client/viscera/boot_source_selections.py | 2 +- maas/client/viscera/nodes.py | 8 +++++++- maas/client/viscera/testing/__init__.py | 3 ++- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/maas/client/bones/__init__.py b/maas/client/bones/__init__.py index 28f26cf..d4633a6 100644 --- a/maas/client/bones/__init__.py +++ b/maas/client/bones/__init__.py @@ -7,7 +7,9 @@ __all__ = ["CallError", "SessionAPI"] import typing -from collections import Iterable, namedtuple + +from collections import namedtuple +from collections.abc import Iterable import json from urllib.parse import urlparse diff --git a/maas/client/utils/__init__.py b/maas/client/utils/__init__.py index bb9d531..290d20f 100644 --- a/maas/client/utils/__init__.py +++ b/maas/client/utils/__init__.py @@ -26,7 +26,9 @@ "vars_class", ] -from collections import Iterable, namedtuple + +from collections import namedtuple +from collections.abc import Iterable from functools import lru_cache, partial from inspect import cleandoc, getdoc from itertools import chain, cycle, repeat @@ -175,7 +177,7 @@ def sign(uri, headers, credentials): docstring = namedtuple("docstring", ("title", "body")) -@lru_cache(2 ** 10) +@lru_cache(2**10) def parse_docstring(thing): """Parse a Python docstring, or the docstring found on `thing`. diff --git a/maas/client/utils/multipart.py b/maas/client/utils/multipart.py index 2e6437c..53f5ea5 100644 --- a/maas/client/utils/multipart.py +++ b/maas/client/utils/multipart.py @@ -16,7 +16,7 @@ __all__ = ["encode_multipart_data"] -from collections import Iterable, Mapping +from collections.abc import Iterable, Mapping from email.generator import BytesGenerator from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart diff --git a/maas/client/viscera/__init__.py b/maas/client/viscera/__init__.py index 6a6fd92..f9352f5 100644 --- a/maas/client/viscera/__init__.py +++ b/maas/client/viscera/__init__.py @@ -19,7 +19,8 @@ "OriginBase", ] -from collections import defaultdict, Iterable, Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence +from collections import defaultdict from copy import copy from datetime import datetime from functools import wraps diff --git a/maas/client/viscera/boot_source_selections.py b/maas/client/viscera/boot_source_selections.py index a550fed..818e674 100644 --- a/maas/client/viscera/boot_source_selections.py +++ b/maas/client/viscera/boot_source_selections.py @@ -2,8 +2,8 @@ __all__ = ["BootSourceSelection", "BootSourceSelections"] -from collections import Sequence +from collections.abc import Sequence from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType from .boot_sources import BootSource diff --git a/maas/client/viscera/nodes.py b/maas/client/viscera/nodes.py index 12c8a00..3b18147 100644 --- a/maas/client/viscera/nodes.py +++ b/maas/client/viscera/nodes.py @@ -2,7 +2,13 @@ __all__ = ["Node", "Nodes"] -from collections import Sequence +try: + # Python <= 3.9 + from collections import Sequence +except: + # Python > 3.9 + from collections.abc import Sequence + import typing from . import ( diff --git a/maas/client/viscera/testing/__init__.py b/maas/client/viscera/testing/__init__.py index eb40d06..f11ed71 100644 --- a/maas/client/viscera/testing/__init__.py +++ b/maas/client/viscera/testing/__init__.py @@ -2,7 +2,8 @@ __all__ = ["bind"] -from collections import Mapping + +from collections.abc import Mapping from itertools import chain from unittest.mock import Mock From e0211170b9f92470b2ab4f22067cecf4107f6a63 Mon Sep 17 00:00:00 2001 From: Alberto Donato Date: Mon, 11 Apr 2022 19:19:28 +0200 Subject: [PATCH 09/29] add python3.10 to tests (#272) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 847e4ea..98a7de1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: - "3.7" - "3.8" - "3.9" + - "3.10" steps: - name: Repository checkout uses: actions/checkout@v2 From 4dc66b5dfaf9fb2f65413d64db37f3e3771d87fa Mon Sep 17 00:00:00 2001 From: Alberto Donato Date: Tue, 31 May 2022 14:22:31 +0200 Subject: [PATCH 10/29] Release 0.6.7 (#279) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5b642c7..72f6070 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(filename): author='MAAS Developers', author_email='maas-devel@lists.launchpad.net', url='https://github.com/maas/python-libmaas', - version="0.6.6", + version="0.6.7", classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', From 69c51b5e64e7ffa09ad35cf14cbaa1bd27e77bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20=C3=96zyurt?= <46789847+canozyurt@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:44:06 +0300 Subject: [PATCH 11/29] prevent expand() iterating through string value #277 (#278) --- maas/client/bones/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maas/client/bones/__init__.py b/maas/client/bones/__init__.py index d4633a6..ba16793 100644 --- a/maas/client/bones/__init__.py +++ b/maas/client/bones/__init__.py @@ -417,7 +417,7 @@ def prepare(self, data): def expand(data): for name, value in data.items(): - if isinstance(value, Iterable): + if isinstance(value, Iterable) and not isinstance(value, str): for value in value: yield name, value else: From fb06cab6227a5dd1979551a2e92739424200ad10 Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Tue, 16 Aug 2022 11:03:07 +0100 Subject: [PATCH 12/29] Fix typo in test configuration --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98a7de1..f65d600 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,5 +53,5 @@ jobs: - name: Test run: | - tox -e py + tox -e py3 codecov From a0673683cf42120852bb8213da6fe5757faf5012 Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Tue, 16 Aug 2022 11:07:11 +0100 Subject: [PATCH 13/29] More Python 3.10 fixes (#282) Fixes #280 --- maas/client/flesh/tabular.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/maas/client/flesh/tabular.py b/maas/client/flesh/tabular.py index 91f202c..a59c097 100644 --- a/maas/client/flesh/tabular.py +++ b/maas/client/flesh/tabular.py @@ -4,6 +4,7 @@ from abc import ABCMeta, abstractmethod import collections +from collections.abc import Iterable import csv import enum from io import StringIO @@ -297,9 +298,7 @@ def render(self, target, datum): elif target is RenderTarget.json: return datum elif target is RenderTarget.csv: - if isinstance(datum, collections.Iterable) and not isinstance( - datum, (str, bytes) - ): + if isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)): return ",".join(datum) else: return datum @@ -308,9 +307,7 @@ def render(self, target, datum): return "" elif isinstance(datum, colorclass.Color): return datum.value_no_colors - elif isinstance(datum, collections.Iterable) and not isinstance( - datum, (str, bytes) - ): + elif isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)): return "\n".join(datum) else: return str(datum) @@ -319,9 +316,7 @@ def render(self, target, datum): return "" elif isinstance(datum, colorclass.Color): return datum - elif isinstance(datum, collections.Iterable) and not isinstance( - datum, (str, bytes) - ): + elif isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)): return "\n".join(datum) else: return str(datum) From babd97f5ff7c5cec8750d3bdb37283bd10e0f515 Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Wed, 17 Aug 2022 11:39:15 +0100 Subject: [PATCH 14/29] Release 0.6.8 (#283) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72f6070..6a6ac1a 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(filename): author='MAAS Developers', author_email='maas-devel@lists.launchpad.net', url='https://github.com/maas/python-libmaas', - version="0.6.7", + version="0.6.8", classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', From 7f5bffa80fd512932604f2f8c5167b5107e0b371 Mon Sep 17 00:00:00 2001 From: Jacopo Rota Date: Thu, 13 Jul 2023 22:35:38 +0200 Subject: [PATCH 15/29] Update gh actions version (#288) * update gh actions version --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f65d600..ca33b92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,13 @@ on: jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Repository checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.6" @@ -29,7 +29,7 @@ jobs: tox -e lint test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: @@ -40,10 +40,10 @@ jobs: - "3.10" steps: - name: Repository checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} From 26f533a52fd5e6ebe91906b310e099e90c7a6326 Mon Sep 17 00:00:00 2001 From: Anton Troyanov Date: Fri, 14 Jul 2023 18:05:50 +0400 Subject: [PATCH 16/29] fix: use MAAS URL to build absolute uri (#287) When MAAS is behind a load-balancer or proxy (e.g. HAProxy with `mode tcp`), we should use MAAS URL stored in profile to build absolute URI of a resource, instead of relying on `uri` property returned by `/describe`. Co-authored-by: Jacopo Rota --- maas/client/bones/__init__.py | 25 +++++++++++++++---------- maas/client/bones/tests/test.py | 12 ++++++++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/maas/client/bones/__init__.py b/maas/client/bones/__init__.py index ba16793..7b5b8d3 100644 --- a/maas/client/bones/__init__.py +++ b/maas/client/bones/__init__.py @@ -38,7 +38,7 @@ async def fromURL(cls, url, *, credentials=None, insecure=False): # For now just re-raise as SessionError. raise SessionError(str(error)) else: - session = cls(description, credentials) + session = cls(url, description, credentials) session.scheme = urlparse(url).scheme session.insecure = insecure return session @@ -49,7 +49,7 @@ def fromProfile(cls, profile): :see: `ProfileStore`. """ - session = cls(profile.description, profile.credentials) + session = cls(profile.url, profile.description, profile.credentials) session.scheme = urlparse(profile.url).scheme session.insecure = profile.other.get("insecure", False) return session @@ -75,7 +75,7 @@ async def login(cls, url, *, username=None, password=None, insecure=False): profile = await helpers.login( url=url, username=username, password=password, insecure=insecure ) - session = cls(profile.description, profile.credentials) + session = cls(url, profile.description, profile.credentials) session.scheme = urlparse(url).scheme session.insecure = insecure return profile, session @@ -90,7 +90,7 @@ async def connect(cls, url, *, apikey=None, insecure=False): instance made using the profile. """ profile = await helpers.connect(url=url, apikey=apikey, insecure=insecure) - session = cls(profile.description, profile.credentials) + session = cls(url, profile.description, profile.credentials) session.scheme = urlparse(url).scheme session.insecure = insecure return profile, session @@ -100,13 +100,15 @@ async def connect(cls, url, *, apikey=None, insecure=False): insecure = False debug = False - def __init__(self, description, credentials=None): + def __init__(self, url, description, credentials=None): """Construct a `SessionAPI`. + :param url: MAAS URL :param description: The description of the remote API. See `fromURL`. :param credentials: Credentials for the remote system. Optional. """ super(SessionAPI, self).__init__() + self.__url = url self.__description = description self.__credentials = credentials self.__populate() @@ -116,15 +118,15 @@ def __populate(self): if self.__credentials is None: for resource in resources: if resource["anon"] is not None: - handler = HandlerAPI(resource["anon"], resource, self) + handler = HandlerAPI(self.__url, resource["anon"], resource, self) setattr(self, handler.name, handler) else: for resource in resources: if resource["auth"] is not None: - handler = HandlerAPI(resource["auth"], resource, self) + handler = HandlerAPI(self.__url, resource["auth"], resource, self) setattr(self, handler.name, handler) elif resource["anon"] is not None: - handler = HandlerAPI(resource["anon"], resource, self) + handler = HandlerAPI(self.__url, resource["anon"], resource, self) setattr(self, handler.name, handler) @property @@ -154,9 +156,10 @@ class HandlerAPI: operations. """ - def __init__(self, handler, resource, session): + def __init__(self, url, handler, resource, session): """Construct a `HandlerAPI`. + :param url: MAAS URL :param handler: The handler description from the overall API description document. See `SessionAPI`. :param resource: The parent of `handler` in the API description @@ -164,6 +167,7 @@ def __init__(self, handler, resource, session): :param session: The `SessionAPI`. """ super(HandlerAPI, self).__init__() + self.__url = url self.__handler = handler self.__resource = resource self.__session = session @@ -187,7 +191,8 @@ def uri(self): This will typically contain replacement patterns; these are interpolated in `CallAPI`. """ - return self.__handler["uri"] + url = urlparse(self.__url) + return f"{url.scheme}://{url.netloc}{self.__handler['path']}" @property def params(self): diff --git a/maas/client/bones/tests/test.py b/maas/client/bones/tests/test.py index f85b5fc..1d7fa66 100644 --- a/maas/client/bones/tests/test.py +++ b/maas/client/bones/tests/test.py @@ -72,8 +72,10 @@ class TestActionAPI_APIVersions(TestCase): for name, version, description in testing.api_descriptions ) + url = "http://127.0.0.1:8080/MAAS/api/2.0/" + def test__Version_read(self): - session = bones.SessionAPI(self.description) + session = bones.SessionAPI(self.url, self.description) action = session.Version.read self.assertThat( action, @@ -91,7 +93,7 @@ def test__Machines_deployment_status(self): if self.version > (2, 0): self.skipTest("Machines.deployment_status only in <= 2.0") - session = bones.SessionAPI(self.description, ("a", "b", "c")) + session = bones.SessionAPI(self.url, self.description, ("a", "b", "c")) action = session.Machines.deployment_status self.assertThat( action, @@ -106,7 +108,7 @@ def test__Machines_deployment_status(self): ) def test__Machines_power_parameters(self): - session = bones.SessionAPI(self.description, ("a", "b", "c")) + session = bones.SessionAPI(self.url, self.description, ("a", "b", "c")) action = session.Machines.power_parameters self.assertThat( action, @@ -129,9 +131,11 @@ class TestCallAPI_APIVersions(TestCase): for name, version, description in testing.api_descriptions ) + url = "http://127.0.0.1:8080/MAAS/api/2.0/" + def test__marshals_lists_into_query_as_repeat_parameters(self): system_ids = list(str(uuid1()) for _ in range(3)) - session = bones.SessionAPI(self.description, ("a", "b", "c")) + session = bones.SessionAPI(self.url, self.description, ("a", "b", "c")) call = session.Machines.power_parameters.bind() call.dispatch = Mock() From 56914bbb2679e97a1a92573fbc19264ee5114942 Mon Sep 17 00:00:00 2001 From: Constantine Yevdyukhin Date: Mon, 17 Jul 2023 02:52:51 -0500 Subject: [PATCH 17/29] Adds `delete` ssh key method (#260) * Add delete ssh key method --- maas/client/viscera/sshkeys.py | 6 ++++++ maas/client/viscera/tests/test_sshkeys.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/maas/client/viscera/sshkeys.py b/maas/client/viscera/sshkeys.py index 9568104..bcd7409 100644 --- a/maas/client/viscera/sshkeys.py +++ b/maas/client/viscera/sshkeys.py @@ -41,3 +41,9 @@ class SSHKey(Object, metaclass=SSHKeyType): id = ObjectField.Checked("id", check(int), readonly=True) key = ObjectField.Checked("key", check(str), readonly=True) keysource = ObjectField.Checked("keysource", check_optional(str), readonly=True) + + async def delete(self): + """Delete this key.""" + await self._handler.delete( + id=self.id, + ) diff --git a/maas/client/viscera/tests/test_sshkeys.py b/maas/client/viscera/tests/test_sshkeys.py index 3220ee7..db00fba 100644 --- a/maas/client/viscera/tests/test_sshkeys.py +++ b/maas/client/viscera/tests/test_sshkeys.py @@ -48,3 +48,13 @@ def test__sshkey_read(self): key_dict = {"id": key_id, "key": make_string_without_spaces(), "keysource": ""} SSHKey._handler.read.return_value = key_dict self.assertThat(SSHKey.read(id=key_id), Equals(SSHKey(key_dict))) + + def test__sshkey_delete(self): + """SSHKeys.read() returns a single SSH key.""" + SSHKey = make_origin().SSHKey + key_id = random.randint(0, 100) + ssh_key = SSHKey( + {"id": key_id, "key": make_string_without_spaces(), "keysource": ""} + ) + ssh_key.delete() + SSHKey._handler.delete.assert_called_once_with(id=key_id) From fd952190c1c878cc59414e371eae964c205a21ab Mon Sep 17 00:00:00 2001 From: kakkotetsu Date: Tue, 18 Jul 2023 20:51:12 +0900 Subject: [PATCH 18/29] add interface_speed, link_speed attribute of Interface object (#274) Co-authored-by: Jacopo Rota --- maas/client/viscera/interfaces.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maas/client/viscera/interfaces.py b/maas/client/viscera/interfaces.py index e52119e..c9d2a22 100644 --- a/maas/client/viscera/interfaces.py +++ b/maas/client/viscera/interfaces.py @@ -102,6 +102,8 @@ class Interface(Object, metaclass=InterfaceTypeMeta): discovered = ObjectFieldRelatedSet( "discovered", "InterfaceDiscoveredLinks", reverse=None ) + interface_speed = ObjectField.Checked("interface_speed", check(int), readonly=True) + link_speed = ObjectField.Checked("link_speed", check(int), readonly=True) def __repr__(self): return super(Interface, self).__repr__(fields={"name", "mac_address", "type"}) From 1ff28d5b775d8e8ad8d733b6eabb48d6efb03206 Mon Sep 17 00:00:00 2001 From: Jack Lloyd-Walters Date: Thu, 17 Aug 2023 17:54:49 +0100 Subject: [PATCH 19/29] switch to packaging.version (#290) * switch to packagin.version * add packaging to setuptools * update version --- maas/client/viscera/version.py | 4 ++-- scripts/check-imports | 2 +- setup.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/maas/client/viscera/version.py b/maas/client/viscera/version.py index a3fe426..185a4b3 100644 --- a/maas/client/viscera/version.py +++ b/maas/client/viscera/version.py @@ -2,13 +2,13 @@ __all__ = ["Version"] -from distutils.version import StrictVersion +from packaging.version import Version as PackagingVersion from . import Object, ObjectField, ObjectType def parse_version(version): - return StrictVersion(version).version + return PackagingVersion(version).release class VersionType(ObjectType): diff --git a/scripts/check-imports b/scripts/check-imports index daf072f..c6cc390 100755 --- a/scripts/check-imports +++ b/scripts/check-imports @@ -96,7 +96,6 @@ python_standard_libs = [ 'difflib', 'dircache', 'dis', - 'distutils', 'dl', 'doctest', 'DocXMLRPCServer', @@ -203,6 +202,7 @@ python_standard_libs = [ 'optparse', 'os', 'ossaudiodev', + 'packaging', 'parser', 'pathlib', 'pdb', diff --git a/setup.py b/setup.py index 6a6ac1a..935c30c 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ def read(filename): "colorclass >= 1.2.0", "macaroonbakery >= 1.1.3", "oauthlib >= 1.0.3", + "packaging >= 21.3", "pymongo >= 3.5.1", # for bson "pytz >= 2014.10", "PyYAML >= 3.11", From 7c1374b1240fc77d030bcb31584457d0eae6aed2 Mon Sep 17 00:00:00 2001 From: Alexandre Erwin Ittner Date: Mon, 21 Aug 2023 06:33:58 -0300 Subject: [PATCH 20/29] Add read-only 'pod' property to Machine objects (#245) This new property allows the client to know in which pod a virtual machine was commissioned (if any). --- maas/client/viscera/machines.py | 1 + 1 file changed, 1 insertion(+) diff --git a/maas/client/viscera/machines.py b/maas/client/viscera/machines.py index 5445783..db42cc2 100644 --- a/maas/client/viscera/machines.py +++ b/maas/client/viscera/machines.py @@ -373,6 +373,7 @@ class Machine(Node, metaclass=MachineType): status_name = ObjectField.Checked("status_name", check(str), readonly=True) raids = ObjectFieldRelatedSet("raids", "Raids", reverse=None) volume_groups = ObjectFieldRelatedSet("volume_groups", "VolumeGroups", reverse=None) + pod = ObjectFieldRelated("pod", "Pod", readonly=True) async def save(self): """Save the machine in MAAS.""" From fec3b5023f9d618ba61e9d9108a345300216d8c3 Mon Sep 17 00:00:00 2001 From: Stamatis Katsaounis Date: Fri, 22 Sep 2023 13:04:52 +0300 Subject: [PATCH 21/29] Add read objects for dns related MAAS entities (#292) --- maas/client/facade.py | 20 +++++++ maas/client/viscera/__init__.py | 3 ++ maas/client/viscera/dnsresourcerecords.py | 38 +++++++++++++ maas/client/viscera/dnsresources.py | 48 +++++++++++++++++ maas/client/viscera/ip_addresses.py | 54 +++++++++++++++++++ .../viscera/tests/test_dnsresourcerecords.py | 50 +++++++++++++++++ .../client/viscera/tests/test_dnsresources.py | 45 ++++++++++++++++ .../client/viscera/tests/test_ip_addresses.py | 29 ++++++++++ setup.py | 2 +- 9 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 maas/client/viscera/dnsresourcerecords.py create mode 100644 maas/client/viscera/dnsresources.py create mode 100644 maas/client/viscera/ip_addresses.py create mode 100644 maas/client/viscera/tests/test_dnsresourcerecords.py create mode 100644 maas/client/viscera/tests/test_dnsresources.py create mode 100644 maas/client/viscera/tests/test_ip_addresses.py diff --git a/maas/client/facade.py b/maas/client/facade.py index 5eb8324..0a8db25 100644 --- a/maas/client/facade.py +++ b/maas/client/facade.py @@ -104,6 +104,20 @@ def devices(origin): "list": origin.Devices.read, } + @facade + def dnsresources(origin): + return { + "get": origin.DNSResource.read, + "list": origin.DNSResources.read, + } + + @facade + def dnsresourcerecords(origin): + return { + "get": origin.DNSResourceRecord.read, + "list": origin.DNSResourceRecords.read, + } + @facade def domains(origin): return { @@ -172,6 +186,12 @@ def ip_ranges(origin): "list": origin.IPRanges.read, } + @facade + def ip_addresses(origin): + return { + "list": origin.IPAddresses.read, + } + @facade def maas(origin): attrs = ( diff --git a/maas/client/viscera/__init__.py b/maas/client/viscera/__init__.py index f9352f5..40edd6f 100644 --- a/maas/client/viscera/__init__.py +++ b/maas/client/viscera/__init__.py @@ -1168,6 +1168,8 @@ def __init__(self, session): ".boot_sources", ".controllers", ".devices", + ".dnsresources", + ".dnsresourcerecords", ".domains", ".events", ".subnets", @@ -1178,6 +1180,7 @@ def __init__(self, session): ".filesystems", ".interfaces", ".ipranges", + ".ip_addresses", ".logical_volumes", ".maas", ".machines", diff --git a/maas/client/viscera/dnsresourcerecords.py b/maas/client/viscera/dnsresourcerecords.py new file mode 100644 index 0000000..4aae103 --- /dev/null +++ b/maas/client/viscera/dnsresourcerecords.py @@ -0,0 +1,38 @@ +"""Objects for dnsresourcerecords.""" + +__all__ = ["DNSResourceRecord", "DNSResourceRecords"] + +from . import check, check_optional, Object, ObjectField, ObjectSet, ObjectType + + +class DNSResourceRecordType(ObjectType): + """Metaclass for `DNSResourceRecords`.""" + + async def read(cls): + data = await cls._handler.read() + return cls(map(cls._object, data)) + + +class DNSResourceRecords(ObjectSet, metaclass=DNSResourceRecordType): + """The set of dnsresourcerecords stored in MAAS.""" + + +class DNSResourceRecordType(ObjectType): + async def read(cls, id): + data = await cls._handler.read(id=id) + return cls(data) + + +class DNSResourceRecord(Object, metaclass=DNSResourceRecordType): + """A dnsresourcerecord stored in MAAS.""" + + id = ObjectField.Checked("id", check(int), readonly=True, pk=True) + ttl = ObjectField.Checked("ttl", check_optional(int), check_optional(int)) + rrtype = ObjectField.Checked("rrtype", check(str), check(str)) + rrdata = ObjectField.Checked("rrdata", check(str), check(str)) + fqdn = ObjectField.Checked("fqdn", check(str), check(str)) + + def __repr__(self): + return super(DNSResourceRecord, self).__repr__( + fields={"ttl", "rrtype", "rrdata", "fqdn"} + ) diff --git a/maas/client/viscera/dnsresources.py b/maas/client/viscera/dnsresources.py new file mode 100644 index 0000000..97e1e46 --- /dev/null +++ b/maas/client/viscera/dnsresources.py @@ -0,0 +1,48 @@ +"""Objects for dnsresources.""" + +__all__ = ["DNSResource", "DNSResources"] + +from . import ( + check, + check_optional, + Object, + ObjectField, + ObjectSet, + ObjectType, + ObjectFieldRelatedSet, +) + + +class DNSResourceType(ObjectType): + """Metaclass for `DNSResources`.""" + + async def read(cls): + data = await cls._handler.read() + return cls(map(cls._object, data)) + + +class DNSResources(ObjectSet, metaclass=DNSResourceType): + """The set of dnsresources stored in MAAS.""" + + +class DNSResourceType(ObjectType): + async def read(cls, id): + data = await cls._handler.read(id=id) + return cls(data) + + +class DNSResource(Object, metaclass=DNSResourceType): + """A dnsresource stored in MAAS.""" + + id = ObjectField.Checked("id", check(int), readonly=True, pk=True) + address_ttl = ObjectField.Checked( + "address_ttl", check_optional(int), check_optional(int) + ) + fqdn = ObjectField.Checked("fqdn", check(str), check(str)) + ip_addresses = ObjectFieldRelatedSet("ip_addresses", "IPAddresses") + resource_records = ObjectFieldRelatedSet("resource_records", "DNSResourceRecords") + + def __repr__(self): + return super(DNSResource, self).__repr__( + fields={"address_ttl", "fqdn", "ip_addresses", "resource_records"} + ) diff --git a/maas/client/viscera/ip_addresses.py b/maas/client/viscera/ip_addresses.py new file mode 100644 index 0000000..5997e76 --- /dev/null +++ b/maas/client/viscera/ip_addresses.py @@ -0,0 +1,54 @@ +"""Objects for ipaddresses.""" + +__all__ = ["IPAddress", "IPAddresses"] + +from . import ( + check, + parse_timestamp, + Object, + ObjectField, + ObjectSet, + ObjectType, + ObjectFieldRelatedSet, + ObjectFieldRelated, + OriginObjectRef, +) + + +class IPAddressType(ObjectType): + """Metaclass for `IPAddresses`.""" + + async def read(cls): + data = await cls._handler.read() + return cls(map(cls._object, data)) + + +class IPAddresses(ObjectSet, metaclass=IPAddressType): + """The set of ipaddresses stored in MAAS.""" + + _object = OriginObjectRef(name="IPAddress") + + +class IPAddress(Object): + """An ipaddress stored in MAAS.""" + + alloc_type = ObjectField.Checked("alloc_type", check(int), check(int)) + alloc_type_name = ObjectField.Checked("alloc_type_name", check(str), check(str)) + created = ObjectField.Checked("created", parse_timestamp, readonly=True) + ip = ObjectField.Checked("ip", check(str)) + owner = ObjectFieldRelated("owner", "User") + interface_set = ObjectFieldRelatedSet("interface_set", "Interfaces") + subnet = ObjectFieldRelated("subnet", "Subnet", readonly=True, default=None) + + def __repr__(self): + return super(IPAddress, self).__repr__( + fields={ + "alloc_type", + "alloc_type_name", + "created", + "ip", + "owner", + "interface_set", + "subnet", + } + ) diff --git a/maas/client/viscera/tests/test_dnsresourcerecords.py b/maas/client/viscera/tests/test_dnsresourcerecords.py new file mode 100644 index 0000000..aa6045f --- /dev/null +++ b/maas/client/viscera/tests/test_dnsresourcerecords.py @@ -0,0 +1,50 @@ +"""Tests for `maas.client.viscera.dnsresourcerecords`.""" + +import random + +from testtools.matchers import Equals + +from .. import dnsresourcerecords + +from ..testing import bind +from ...testing import make_string_without_spaces, TestCase + + +def make_origin(): + """ + Create a new origin with DNSResourceRecord and DNSResourceRecords. The former + refers to the latter via the origin, hence why it must be bound. + """ + return bind( + dnsresourcerecords.DNSResourceRecords, dnsresourcerecords.DNSResourceRecord + ) + + +class TestDNSResourceRecords(TestCase): + def test__dnsresourcerecords_read(self): + """DNSResourceRecords.read() returns a list of DNSResourceRecords.""" + DNSResourceRecords = make_origin().DNSResourceRecords + dnsresourcerecords = [ + {"id": random.randint(0, 100), "fqdn": make_string_without_spaces()} + for _ in range(3) + ] + DNSResourceRecords._handler.read.return_value = dnsresourcerecords + dnsresourcerecords = DNSResourceRecords.read() + self.assertThat(len(dnsresourcerecords), Equals(3)) + + +class TestDNSResourceRecord(TestCase): + def test__dnsresourcerecord_read(self): + DNSResourceRecord = make_origin().DNSResourceRecord + dnsresourcerecord = { + "id": random.randint(0, 100), + "fqdn": make_string_without_spaces(), + } + DNSResourceRecord._handler.read.return_value = dnsresourcerecord + self.assertThat( + DNSResourceRecord.read(id=dnsresourcerecord["id"]), + Equals(DNSResourceRecord(dnsresourcerecord)), + ) + DNSResourceRecord._handler.read.assert_called_once_with( + id=dnsresourcerecord["id"] + ) diff --git a/maas/client/viscera/tests/test_dnsresources.py b/maas/client/viscera/tests/test_dnsresources.py new file mode 100644 index 0000000..fc197ec --- /dev/null +++ b/maas/client/viscera/tests/test_dnsresources.py @@ -0,0 +1,45 @@ +"""Tests for `maas.client.viscera.dnsresources`.""" + +import random + +from testtools.matchers import Equals + +from .. import dnsresources + +from ..testing import bind +from ...testing import make_string_without_spaces, TestCase + + +def make_origin(): + """ + Create a new origin with DNSResource and DNSResources. The former + refers to the latter via the origin, hence why it must be bound. + """ + return bind(dnsresources.DNSResources, dnsresources.DNSResource) + + +class TestDNSResources(TestCase): + def test__dnsresources_read(self): + """DNSResources.read() returns a list of DNSResources.""" + DNSResources = make_origin().DNSResources + dnsresources = [ + {"id": random.randint(0, 100), "fqdn": make_string_without_spaces()} + for _ in range(3) + ] + DNSResources._handler.read.return_value = dnsresources + dnsresources = DNSResources.read() + self.assertThat(len(dnsresources), Equals(3)) + + +class TestDNSResource(TestCase): + def test__dnsresource_read(self): + DNSResource = make_origin().DNSResource + dnsresource = { + "id": random.randint(0, 100), + "fqdn": make_string_without_spaces(), + } + DNSResource._handler.read.return_value = dnsresource + self.assertThat( + DNSResource.read(id=dnsresource["id"]), Equals(DNSResource(dnsresource)) + ) + DNSResource._handler.read.assert_called_once_with(id=dnsresource["id"]) diff --git a/maas/client/viscera/tests/test_ip_addresses.py b/maas/client/viscera/tests/test_ip_addresses.py new file mode 100644 index 0000000..a5313e6 --- /dev/null +++ b/maas/client/viscera/tests/test_ip_addresses.py @@ -0,0 +1,29 @@ +"""Tests for `maas.client.viscera.ip_addresses`.""" + +from testtools.matchers import Equals + +from .. import ip_addresses + +from ..testing import bind +from ...testing import TestCase + + +def make_origin(): + """ + Create a new origin with IPAddress and IPAddresses. The former + refers to the latter via the origin, hence why it must be bound. + """ + return bind(ip_addresses.IPAddresses, ip_addresses.IPAddress) + + +class TestIPAddresses(TestCase): + def test__ip_addresses_read(self): + """IPAddresses.read() returns a list of IPAddresses.""" + IPAddresses = make_origin().IPAddresses + ip_addresses = [ + {"ip": "10.0.0.%s" % (i + 1), "alloc_type_name": "User reserved"} + for i in range(3) + ] + IPAddresses._handler.read.return_value = ip_addresses + ip_addresses = IPAddresses.read() + self.assertThat(len(ip_addresses), Equals(3)) diff --git a/setup.py b/setup.py index 935c30c..7078483 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def read(filename): "setuptools", "testscenarios", "testtools", - "Twisted", + "Twisted<23.0.0", ], description="A client API library specially for MAAS.", long_description=read('README'), From bfe63ccf3539018a6b2ce3266bafe1fc1c2a9a0a Mon Sep 17 00:00:00 2001 From: Anton Troyanov Date: Thu, 1 Feb 2024 15:06:33 +0400 Subject: [PATCH 22/29] ci: autoclose stale issues (#296) Mark issue as stale if there is no activity for 30 days. Close stale issues after 30 days. This doesn't affect issues with labels: `triaged` or `help wanted` --- .github/workflows/inactiveissue.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/inactiveissue.yml diff --git a/.github/workflows/inactiveissue.yml b/.github/workflows/inactiveissue.yml new file mode 100644 index 0000000..f50f488 --- /dev/null +++ b/.github/workflows/inactiveissue.yml @@ -0,0 +1,27 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 30 + days-before-issue-close: 30 + exempt-issue-labels: "triaged, help wanted" + stale-issue-label: "stale" + stale-issue-message: + "This issue is stale because it has been open for 30 days with no + activity." + close-issue-message: + "This issue was closed because it has been inactive for 30 days + since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} From 18aad070a210a444d4889f2b018cad33b32a5ca2 Mon Sep 17 00:00:00 2001 From: Jack Lloyd-Walters Date: Mon, 15 Apr 2024 09:42:53 +0100 Subject: [PATCH 23/29] migrate repo org (#297) --- README | 6 +++--- debian/control | 2 +- debian/copyright | 2 +- debian/watch | 2 +- doc.yaml | 2 +- doc/index.md | 4 ++-- maas/client/viscera/tests/test_machines.py | 2 +- maas/client/viscera/tests/test_vlans.py | 2 +- setup.py | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README b/README index fb3fce3..3b292a5 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ Python client API library made especially for [MAAS][]. -[![CI tests](https://github.com/maas/python-libmaas/workflows/CI%20tests/badge.svg)](https://github.com/maas/python-libmaas/actions?query=workflow%3A%22CI+tests%22) -[![codecov.io](https://codecov.io/github/maas/python-libmaas/coverage.svg?branch=master)](https://codecov.io/github/maas/python-libmaas?branch=master) +[![CI tests](https://github.com/canonical/python-libmaas/workflows/CI%20tests/badge.svg)](https://github.com/canonical/python-libmaas/actions?query=workflow%3A%22CI+tests%22) +[![codecov.io](https://codecov.io/github/canonical/python-libmaas/coverage.svg?branch=master)](https://codecov.io/github/maas/python-libmaas?branch=master) ## Installation @@ -14,7 +14,7 @@ with [pip](https://pip.pypa.io/). Python 3.5+ is required. When working from master it can be helpful to use a virtualenv: $ python3 -m venv ve && source ve/bin/activate - $ pip install git+https://github.com/maas/python-libmaas.git + $ pip install git+https://github.com/canonical/python-libmaas.git $ maas --help Releases are periodically made to [PyPI](https://pypi.python.org/) but, diff --git a/debian/control b/debian/control index daaebe5..40fe1c6 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Andres Rodriguez Build-Depends: debhelper (>= 10), dh-python, python3-all, python3-setuptools Standards-Version: 4.1.3 -Homepage: https://github.com/maas/python-libmaas +Homepage: https://github.com/canonical/python-libmaas X-Python3-Version: >= 3.2 Package: python3-libmaas diff --git a/debian/copyright b/debian/copyright index 9d51684..8af8f6a 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,6 +1,6 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: python-libmaas -Source: https://github.com/maas/python-libmaas +Source: https://github.com/canonical/python-libmaas Files: * Copyright: 2017-2018 Canonical Ltd. diff --git a/debian/watch b/debian/watch index 78f8221..929cbb7 100644 --- a/debian/watch +++ b/debian/watch @@ -6,5 +6,5 @@ version=4 # GitHub hosted projects opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%python-libmaas-$1.tar.gz%" \ - https://github.com/maas/python-libmaas/tags \ + https://github.com/canonical/python-libmaas/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate diff --git a/doc.yaml b/doc.yaml index 531676a..d39de88 100644 --- a/doc.yaml +++ b/doc.yaml @@ -3,7 +3,7 @@ markdown_extensions: - codehilite - sane_lists - smarty -repo_url: https://github.com/maas/python-libmaas +repo_url: https://github.com/canonical/python-libmaas site_name: MAAS Client Library & CLI strict: true theme: readthedocs diff --git a/doc/index.md b/doc/index.md index 9720c9c..5813d83 100644 --- a/doc/index.md +++ b/doc/index.md @@ -27,7 +27,7 @@ until we release a beta all APIs could change. Either work from a branch: ```console -$ git clone https://github.com/maas/python-libmaas.git +$ git clone https://github.com/canonical/python-libmaas.git $ cd python-libmaas $ make ``` @@ -37,7 +37,7 @@ Or install with [pip](https://pip.pypa.io/) into a ```console $ virtualenv --python=python3 amc && source amc/bin/activate -$ pip install git+https://github.com/maas/python-libmaas.git +$ pip install git+https://github.com/canonical/python-libmaas.git ``` Or install from [PyPI](https://pypi.python.org/): diff --git a/maas/client/viscera/tests/test_machines.py b/maas/client/viscera/tests/test_machines.py index 3748774..939afcc 100644 --- a/maas/client/viscera/tests/test_machines.py +++ b/maas/client/viscera/tests/test_machines.py @@ -203,7 +203,7 @@ def test__commission_with_wait_failed(self): ) def test__commission_with_no_tests(self): - # Regression test for https://github.com/maas/python-libmaas/issues/185 + # Regression test for https://github.com/canonical/python-libmaas/issues/185 system_id = make_name_without_spaces("system-id") hostname = make_name_without_spaces("hostname") data = { diff --git a/maas/client/viscera/tests/test_vlans.py b/maas/client/viscera/tests/test_vlans.py index 9bf4c69..750e708 100644 --- a/maas/client/viscera/tests/test_vlans.py +++ b/maas/client/viscera/tests/test_vlans.py @@ -220,7 +220,7 @@ def test__vlan_update_relay_vlan_with_object(self): self.assertThat(vlan.relay_vlan.id, Equals(relay_vlan.id)) def test__vlan_update_relay_vlan_with_integer_id(self): - self.skip("see https://github.com/maas/python-libmaas/issues/180") + self.skip("see https://github.com/canonical/python-libmaas/issues/180") origin = make_origin() Vlan = origin.Vlan Vlan._handler.params = ["fabric_id", "vid"] diff --git a/setup.py b/setup.py index 7078483..071f10e 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def read(filename): name='python-libmaas', author='MAAS Developers', author_email='maas-devel@lists.launchpad.net', - url='https://github.com/maas/python-libmaas', + url='https://github.com/canonical/python-libmaas', version="0.6.8", classifiers=[ 'Development Status :: 3 - Alpha', From 5eac0fe2d061097aa3b1d36d764a7103987f0909 Mon Sep 17 00:00:00 2001 From: Alan Baghumian Date: Mon, 27 May 2024 15:50:11 -0700 Subject: [PATCH 24/29] Add support for ephemeral_deploy and enable_hw_sync parameters to deploy() (#302) --- maas/client/viscera/machines.py | 11 +++++- maas/client/viscera/tests/test_machines.py | 40 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/maas/client/viscera/machines.py b/maas/client/viscera/machines.py index db42cc2..f9ac96d 100644 --- a/maas/client/viscera/machines.py +++ b/maas/client/viscera/machines.py @@ -480,7 +480,9 @@ async def deploy( comment: str = None, wait: bool = False, install_kvm: bool = False, - wait_interval: int = 5 + wait_interval: int = 5, + ephemeral_deploy: bool = False, + enable_hw_sync: bool = False ): """Deploy this machine. @@ -494,6 +496,8 @@ async def deploy( :param comment: A comment for the event log. :param wait: If specified, wait until the deploy is complete. :param wait_interval: How often to poll, defaults to 5 seconds + :param ephemeral_deploy: Deploy a machine in Ephemeral mode + :param enable_hw_sync: Enables periodic hardware sync """ params = {"system_id": self.system_id} @@ -513,6 +517,11 @@ async def deploy( params["hwe_kernel"] = hwe_kernel if comment is not None: params["comment"] = comment + if ephemeral_deploy: + params["ephemeral_deploy"] = ephemeral_deploy + if enable_hw_sync: + params["enable_hw_sync"] = enable_hw_sync + self._reset(await self._handler.deploy(**params)) if not wait: return self diff --git a/maas/client/viscera/tests/test_machines.py b/maas/client/viscera/tests/test_machines.py index 939afcc..62d97ad 100644 --- a/maas/client/viscera/tests/test_machines.py +++ b/maas/client/viscera/tests/test_machines.py @@ -259,6 +259,46 @@ def test__deploy_with_kvm_install(self): system_id=machine.system_id, install_kvm=True ) + def test__deploy_with_ephemeral_deploy(self): + system_id = make_name_without_spaces("system-id") + hostname = make_name_without_spaces("hostname") + data = { + "system_id": system_id, + "hostname": hostname, + "status": NodeStatus.READY, + } + deploying_data = { + "system_id": system_id, + "hostname": hostname, + "status": NodeStatus.DEPLOYING, + } + machine = make_machines_origin().Machine(data) + machine._handler.deploy.return_value = deploying_data + machine.deploy(ephemeral_deploy=True, wait=False) + machine._handler.deploy.assert_called_once_with( + system_id=machine.system_id, ephemeral_deploy=True + ) + + def test__deploy_with_enable_hw_sync(self): + system_id = make_name_without_spaces("system-id") + hostname = make_name_without_spaces("hostname") + data = { + "system_id": system_id, + "hostname": hostname, + "status": NodeStatus.READY, + } + deploying_data = { + "system_id": system_id, + "hostname": hostname, + "status": NodeStatus.DEPLOYING, + } + machine = make_machines_origin().Machine(data) + machine._handler.deploy.return_value = deploying_data + machine.deploy(enable_hw_sync=True, wait=False) + machine._handler.deploy.assert_called_once_with( + system_id=machine.system_id, enable_hw_sync=True + ) + def test__deploy_with_wait_failed(self): system_id = make_name_without_spaces("system-id") hostname = make_name_without_spaces("hostname") From 7160c93f3edf5525030a62ab1cf20a3637017965 Mon Sep 17 00:00:00 2001 From: Gary Miguel Date: Tue, 28 May 2024 00:28:24 -0700 Subject: [PATCH 25/29] Fix: spinner using invalid escape sequence (#298) Before this running this code would result in: ``` maas/client/utils/__init__.py:341: SyntaxWarning: invalid escape sequence '\|' ``` Co-authored-by: Jacopo Rota --- maas/client/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maas/client/utils/__init__.py b/maas/client/utils/__init__.py index 290d20f..1ae5a92 100644 --- a/maas/client/utils/__init__.py +++ b/maas/client/utils/__init__.py @@ -338,7 +338,7 @@ class Spinner: Use as a context manager. """ - def __init__(self, frames="/-\|", stream=sys.stdout): + def __init__(self, frames=r"/-\|", stream=sys.stdout): super(Spinner, self).__init__() self.frames = frames self.stream = stream From 410f9f9aae11bcaaff9455b7a7bfb02249c7b1ef Mon Sep 17 00:00:00 2001 From: Stamatis Katsaounis Date: Tue, 28 May 2024 10:44:37 +0300 Subject: [PATCH 26/29] ci: add Canonical CLA check (#303) --- .github/workflows/cla-check.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/cla-check.yml diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml new file mode 100644 index 0000000..f79e0d9 --- /dev/null +++ b/.github/workflows/cla-check.yml @@ -0,0 +1,12 @@ +name: cla-check + +on: + pull_request: + branches: [master] + +jobs: + cla-check: + runs-on: ubuntu-latest + steps: + - name: Check if CLA signed + uses: canonical/has-signed-canonical-cla@v1 From 627b3fcf53974ce6fe111e4f02f76b7d951c1ffe Mon Sep 17 00:00:00 2001 From: Anton Troyanov Date: Wed, 17 Jul 2024 12:27:20 +0400 Subject: [PATCH 27/29] ci: use `pull_request_target` for cla-check (#306) When run on the `pull_request_target` event, `has-signed-canonical-cla` action will also comment on the PR if any authors have not signed the CLA, and update those messages when new commits or runs are processed. --- .github/workflows/cla-check.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index f79e0d9..aeef9a8 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -1,8 +1,6 @@ name: cla-check -on: - pull_request: - branches: [master] +on: [pull_request_target] jobs: cla-check: From 4ead68909b76bb19ad1fad972ac1698acfd4605c Mon Sep 17 00:00:00 2001 From: Anton Troyanov Date: Mon, 31 Mar 2025 23:34:02 +0400 Subject: [PATCH 28/29] ci: autoclose uses maas-github-workflows --- .github/workflows/inactiveissue.yml | 27 --------------------------- .github/workflows/stale-cron.yaml | 9 +++++++++ 2 files changed, 9 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/inactiveissue.yml create mode 100644 .github/workflows/stale-cron.yaml diff --git a/.github/workflows/inactiveissue.yml b/.github/workflows/inactiveissue.yml deleted file mode 100644 index f50f488..0000000 --- a/.github/workflows/inactiveissue.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Close inactive issues -on: - schedule: - - cron: "30 1 * * *" - -jobs: - close-issues: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/stale@v9 - with: - days-before-issue-stale: 30 - days-before-issue-close: 30 - exempt-issue-labels: "triaged, help wanted" - stale-issue-label: "stale" - stale-issue-message: - "This issue is stale because it has been open for 30 days with no - activity." - close-issue-message: - "This issue was closed because it has been inactive for 30 days - since being marked as stale." - days-before-pr-stale: -1 - days-before-pr-close: -1 - repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale-cron.yaml b/.github/workflows/stale-cron.yaml new file mode 100644 index 0000000..f557996 --- /dev/null +++ b/.github/workflows/stale-cron.yaml @@ -0,0 +1,9 @@ +name: Close inactive issues +on: + schedule: + - cron: "0 0 * * *" + +jobs: + close-issues: + uses: canonical/maas-github-workflows/.github/workflows/stale-cron.yaml@v0 + secrets: inherit From b13bac86220f43f4dc1102317c4429f2cd9a1f96 Mon Sep 17 00:00:00 2001 From: Stamatis Katsaounis Date: Tue, 2 Sep 2025 11:05:10 +0300 Subject: [PATCH 29/29] chore: update CLA check (#312) --- .github/workflows/cla-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index aeef9a8..6b767cc 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -1,10 +1,10 @@ name: cla-check -on: [pull_request_target] +on: [pull_request] jobs: cla-check: runs-on: ubuntu-latest steps: - name: Check if CLA signed - uses: canonical/has-signed-canonical-cla@v1 + uses: canonical/has-signed-canonical-cla@v2