diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 00000000..216dfef0
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,11 @@
+[run]
+branch = True
+source =
+ maas/client
+omit =
+ */testing.py
+ */tests/*.py
+
+[html]
+title =
+ Coverage for python-libmaas
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..329b40dc
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+/debian export-ignore
+/.git export-ignore
+.gitignore export-ignore
+.gitattributes export-ignore
+.travis.yml export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..ca33b927
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: CI tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ lint:
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Repository checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.6"
+
+ - name: Install dependencies
+ run: |
+ pip install --upgrade pip tox
+
+ - name: Lint
+ run: |
+ tox -e lint
+
+ test:
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ python-version:
+ - "3.6"
+ - "3.7"
+ - "3.8"
+ - "3.9"
+ - "3.10"
+ steps:
+ - name: Repository checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ pip install --upgrade pip tox codecov
+
+ - name: Test
+ run: |
+ tox -e py3
+ codecov
diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml
new file mode 100644
index 00000000..6b767cce
--- /dev/null
+++ b/.github/workflows/cla-check.yml
@@ -0,0 +1,10 @@
+name: cla-check
+
+on: [pull_request]
+
+jobs:
+ cla-check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check if CLA signed
+ uses: canonical/has-signed-canonical-cla@v2
diff --git a/.github/workflows/stale-cron.yaml b/.github/workflows/stale-cron.yaml
new file mode 100644
index 00000000..f5579966
--- /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
diff --git a/.gitignore b/.gitignore
index baa9aeb4..3451c7f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ bin/
include/
local/
share/
+*.snap
# PyInstaller
# Usually these files are written by a python script from a template
@@ -63,3 +64,38 @@ target/
# mkdocs
site/
+
+# Packaging
+.pc/
+
+# Editors
+.vscode/
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/
+
+
+# CMake
+cmake-build-*/
+
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 94b7aa83..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: python
-python:
- - "3.5"
-
-install:
- - pip install codecov tox
-
-script:
- - tox -e py35,lint
-
-after_success:
- - codecov --env TRAVIS_PYTHON_VERSION
-
-branches:
- only:
- - master
diff --git a/LICENSE b/LICENSE
index c20b2206..2c315702 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
-Alburnum MAAS Client is Copyright 2015-2016 Gavin Panella and Copyright
-2015-2016 Canonical Ltd.
+MAAS Client is Copyright 2015-2016 Gavin Panella and Copyright
+2015-2017 Canonical Ltd.
-Gavin Panella and Canonical Ltd. distributes the Alburnum MAAS Client
+Gavin Panela and Canonical Ltd. distributes the MAAS Client
source code under the GNU Affero General Public License, version 3
("AGPLv3"). The full text of this licence is given below.
diff --git a/Makefile b/Makefile
index 83053c07..774315ab 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,10 @@
-PYTHON := python3.5
+python := python3
+snapcraft := SNAPCRAFT_BUILD_INFO=1 /snap/bin/snapcraft
+
+# ---
+
+install-dependencies:
+ if [ -x /usr/bin/snap ]; then sudo snap install --classic snapcraft; fi
# ---
@@ -6,16 +12,23 @@ develop: bin/python setup.py
bin/python setup.py develop
dist: bin/python setup.py README
- bin/python setup.py egg_info sdist
+ bin/python setup.py sdist bdist_wheel
-upload: bin/python setup.py README
- bin/python setup.py egg_info sdist upload
+upload: bin/python bin/twine setup.py README
+ bin/python setup.py sdist bdist_wheel
+ bin/twine upload dist/*
test: bin/tox
@bin/tox
+integrate: bin/tox
+ @bin/tox -e integrate
+
+format: bin/tox
+ @bin/tox -e format,imports
+
lint: bin/tox
- @bin/tox -e lint
+ @bin/tox -e lint,imports
clean:
$(RM) -r bin build dist include lib local share
@@ -25,6 +38,15 @@ clean:
find . -name '*.egg-info' -print0 | xargs -r0 $(RM) -r
find . -name '*~' -print0 | xargs -r0 $(RM)
$(RM) -r .eggs .tox .coverage TAGS tags
+ $(RM) pip-selfcheck.json
+
+# ---
+
+snap-clean:
+ $(snapcraft) clean
+
+snap:
+ $(snapcraft)
# ---
@@ -34,17 +56,36 @@ README: README.md
docs: bin/mkdocs
bin/mkdocs build --config-file doc.yaml --clean --strict
+docs-to-github: bin/mkdocs
+ bin/mkdocs gh-deploy --config-file doc.yaml --clean
+
# ---
bin/tox: bin/pip
bin/pip install --quiet --ignore-installed tox
bin/python bin/pip:
- virtualenv --python=$(PYTHON) --quiet $(CURDIR)
+ virtualenv --python=$(python) --quiet $(CURDIR)
bin/mkdocs: bin/pip
bin/pip install --quiet --ignore-installed "mkdocs >= 0.14.0"
+bin/twine: bin/pip
+ bin/pip install --quiet --ignore-installed twine
+
+# ---
+
+api-json-raw := $(wildcard maas/client/bones/testing/*.raw.json)
+api-json := $(patsubst %.raw.json,%.json,$(api-json-raw))
+
+pretty: $(api-json)
+
+%.json: %.pretty.json
+ cp $^ $@
+
+%.pretty.json: %.raw.json
+ scripts/prettify-api-desc-doc < $^ > $@
+
# ---
-.PHONY: develop dist docs test lint clean
+.PHONY: install-dependencies develop dist docs docs-to-github test integrate lint clean pretty snap snap-clean
diff --git a/README b/README
index 229b0735..3b292a58 100644
--- a/README
+++ b/README
@@ -1,45 +1,67 @@
-python-libmaas
-==============
+# python-libmaas
-Python client API library made especially for
-`MAAS `__.
+Python client API library made especially for [MAAS][].
-This was begun by a core MAAS developer, Gavin Panella, on his own time,
-but is now maintained by the core MAAS team at Canonical. It is licensed
-under the GNU Affero GPLv3, the same as MAAS itself.
+[](https://github.com/canonical/python-libmaas/actions?query=workflow%3A%22CI+tests%22)
+[](https://codecov.io/github/maas/python-libmaas?branch=master)
-|Build Status| |codecov.io|
-Some of the code in here has come from MAAS, upon which Canonical Ltd
-has the copyright. Gavin Panella licenses his parts under the AGPLv3,
-and MAAS is also under the AGPLv3, so everything should be good.
+## Installation
-Installation
-------------
+All the dependencies are declared in `setup.py` so this can be installed
+with [pip](https://pip.pypa.io/). Python 3.5+ is required.
-All the dependencies are declared in ``setup.py`` so this can be
-installed with `pip `__. Python 3.5 is required.
+When working from master it can be helpful to use a virtualenv:
-When working from trunk it can be helpful to use ``virtualenv``:
+ $ python3 -m venv ve && source ve/bin/activate
+ $ pip install git+https://github.com/canonical/python-libmaas.git
+ $ maas --help
-::
+Releases are periodically made to [PyPI](https://pypi.python.org/) but,
+at least for now, it makes more sense to work directly from trunk.
- $ virtualenv --python=python3.5 amc && source amc/bin/activate
- $ pip install git+https://github.com/maas/python-libmaas.git
- $ maas --help
-Releases are periodically made to `PyPI `__
-but, at least for now, it makes more sense to work directly from trunk.
+## Documentation
+
+Documentation can be generated with `make docs` which publishes into the
+`site` directory. Recent documentation is also published to the
+[MAAS Client Library & CLI documentation][docs] site.
+
+
+## Development
+
+It's pretty easy to start hacking on _python-libmaas_:
+
+ $ git clone git@github.com:maas/python-libmaas.git
+ $ cd python-libmaas
+ $ make develop
+ $ make test
+
+Installing [IPython][] is generally a good idea too:
+
+ $ bin/pip install -UI IPython
+
+Pull requests are welcome but authors need to sign the [Canonical
+contributor license agreement][CCLA] before those PRs can be merged.
+
+
+## History & licence
+
+In short: [AGPLv3][].
+
+_python-libmaas_ was begun by a core MAAS developer, Gavin Panella, on
+his own time, but is now maintained by the core MAAS team at Canonical.
+It is licensed under the GNU Affero GPLv3, the same as MAAS itself.
+
+Some of the code in here has come from MAAS, upon which Canonical Ltd
+has the copyright. Gavin Panella licenses his parts under the AGPLv3,
+and MAAS is also under the AGPLv3, so everything should be good.
+
-Documentation
--------------
+[MAAS]: https://maas.io/
+[docs]: http://maas.github.io/python-libmaas/
-Documentation can be generated with ``make docs`` which publishes into
-the ``site`` directory. Recent documentation is also published to the
-`MAAS Client Library & CLI
-documentation `__ site.
+[CCLA]: https://www.ubuntu.com/legal/contributors
+[AGPLv3]: https://www.gnu.org/licenses/agpl-3.0.html
-.. |Build Status| image:: https://travis-ci.org/maas/python-libmaas.svg?branch=master
- :target: https://travis-ci.org/maas/python-libmaas
-.. |codecov.io| image:: https://codecov.io/github/maas/python-libmaas/coverage.svg?branch=master
- :target: https://codecov.io/github/maas/python-libmaas?branch=master
+[IPython]: https://ipython.org/
diff --git a/README.md b/README.md
deleted file mode 100644
index 661a05a9..00000000
--- a/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# python-libmaas
-
-Python client API library made especially for [MAAS][1].
-
-This was begun by a core MAAS developer, Gavin Panella, on his own time,
-but is now maintained by the core MAAS team at Canonical. It is licensed
-under the GNU Affero GPLv3, the same as MAAS itself.
-
-[](https://travis-ci.org/maas/python-libmaas)
-[](https://codecov.io/github/maas/python-libmaas?branch=master)
-
-Some of the code in here has come from MAAS, upon which Canonical Ltd
-has the copyright. Gavin Panella licenses his parts under the AGPLv3,
-and MAAS is also under the AGPLv3, so everything should be good.
-
-
-## Installation
-
-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`:
-
- $ virtualenv --python=python3.5 amc && source amc/bin/activate
- $ pip install git+https://github.com/maas/python-libmaas.git
- $ maas --help
-
-Releases are periodically made to [PyPI](https://pypi.python.org/) but,
-at least for now, it makes more sense to work directly from trunk.
-
-
-## Documentation
-
-Documentation can be generated with `make docs` which publishes into the
-`site` directory. Recent documentation is also published to the
-[MAAS Client Library & CLI documentation][2] site.
-
-
-[1]: https://maas.ubuntu.com/
-[2]: http://maas.github.io/python-libmaas/
diff --git a/README.md b/README.md
new file mode 120000
index 00000000..100b9382
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+README
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 00000000..98e04531
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,14 @@
+python-libmaas (0.6.0-0ubuntu1) bionic; urgency=medium
+
+ * New upstream release
+ * d/p/{00-disable_bson_install_requires,01-fix_setup_py_lists}.patch: Drop.
+ merged upstream.
+ * debian/watch: Cleanup unneeded comments.
+
+ -- Andres Rodriguez Tue, 06 Feb 2018 21:15:00 -0500
+
+python-libmaas (0.5.0-0ubuntu1) bionic; urgency=medium
+
+ * Initial release (LP: #1747328)
+
+ -- Andres Rodriguez Sun, 04 Feb 2018 20:45:42 -0500
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 00000000..f599e28b
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+10
diff --git a/debian/control b/debian/control
new file mode 100644
index 00000000..40fe1c62
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,26 @@
+Source: python-libmaas
+Section: python
+Priority: optional
+Maintainer: Andres Rodriguez
+Build-Depends: debhelper (>= 10), dh-python, python3-all, python3-setuptools
+Standards-Version: 4.1.3
+Homepage: https://github.com/canonical/python-libmaas
+X-Python3-Version: >= 3.2
+
+Package: python3-libmaas
+Architecture: all
+Depends: python3-aiohttp,
+ python3-argcomplete,
+ python3-bson,
+ python3-colorclass,
+ python3-oauthlib,
+ python3-tz,
+ python3-yaml,
+ python3-terminaltables,
+ ${python3:Depends},
+ ${misc:Depends}
+Description: MAAS asyncio client library (Python 3)
+ The MAAS Python Client library provides an asyncio based library
+ to interact with MAAS.
+ .
+ This package installs the library for Python 3.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 00000000..8af8f6a0
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,26 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: python-libmaas
+Source: https://github.com/canonical/python-libmaas
+
+Files: *
+Copyright: 2017-2018 Canonical Ltd.
+License: AGPL-3.0+
+
+Files: debian/*
+Copyright: 2018 Andres Rodriguez
+ 2018 Canonical Ltd.
+License: AGPL-3.0+
+
+License: AGPL-3.0+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of the
+ License, or (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+ .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
diff --git a/debian/files b/debian/files
new file mode 100644
index 00000000..1ee54ad2
--- /dev/null
+++ b/debian/files
@@ -0,0 +1 @@
+python-libmaas_0.6.0-0ubuntu1_source.buildinfo python optional
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 00000000..e69de29b
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 00000000..8cddcb21
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,24 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+#export DH_VERBOSE = 1
+
+export PYBUILD_NAME=python-libmaas
+
+%:
+ dh $@ --with python3 --buildsystem=pybuild
+
+
+override_dh_auto_install:
+ dh_auto_install
+
+ # Remove binary that's created by the setup.py
+ rm -rf $(CURDIR)/debian/python3-libmaas/usr/bin/maas
+
+override_dh_auto_build:
+ # Do nothing. We have nothing to build, hence disabling
+ # the build process.
+
+override_dh_auto_test:
+ # Do nothing. Tests require running daemons, hence
+ # disable them during packaging building.
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 00000000..163aaf8d
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/source/options b/debian/source/options
new file mode 100644
index 00000000..cb61fa52
--- /dev/null
+++ b/debian/source/options
@@ -0,0 +1 @@
+extend-diff-ignore = "^[^/]*[.]egg-info/"
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 00000000..929cbb7a
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,10 @@
+# Compulsory line, this is a version 4 file
+version=4
+
+# PGP signature mangle, so foo.tar.gz has foo.tar.gz.sig
+#opts="pgpsigurlmangle=s%$%.sig%"
+
+# GitHub hosted projects
+opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%python-libmaas-$1.tar.gz%" \
+ https://github.com/canonical/python-libmaas/tags \
+ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
diff --git a/doc.yaml b/doc.yaml
index cc633985..d39de881 100644
--- a/doc.yaml
+++ b/doc.yaml
@@ -3,18 +3,20 @@ 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
use_directory_urls: false
pages:
- Home: index.md
- - Bones:
- - Home: bones/index.md
- - Viscera:
- - Home: viscera/index.md
- - Getting started: viscera/getting-started.md
- - Nodes: viscera/nodes.md
- - Events: viscera/events.md
- - Other objects: viscera/other.md
+ - Client:
+ - Introduction: client/index.md
+ - Nodes: client/nodes.md
+ - Networking: client/networking.md
+ - Interfaces: client/interfaces.md
+ - Events: client/events.md
+ - Others: client/other.md
+ - Development:
+ - Release checklist: development/releasing.md
+ - Adding an object: development/adding-an-object.md
diff --git a/doc/bones/index.md b/doc/bones/index.md
deleted file mode 100644
index 42e7e925..00000000
--- a/doc/bones/index.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# _Bones_: Low-level Python client API
-
-You may prefer the [higher-level API _viscera_](../viscera/index.md),
-but maybe you need to do something that you can't do in _viscera_ yet
-(please file a bug!), or you're developing _viscera_ itself (which uses
-_bones_ behind the scenes).
-
-
-## Some example code
-
-```python
-#!/usr/bin/env python3.5
-
-from http import HTTPStatus
-from pprint import pprint
-
-from maas.client import bones
-
-
-profile, session = bones.SessionAPI.login(
- "http://localhost:5240/MAAS/", username="alice",
- password="wonderland")
-
-# Create a tag if it doesn't exist.
-tag_name = "gryphon"
-tag_comment = "Gryphon's Stuff"
-try:
- tag = session.Tag.read(name=tag_name)
-except bones.CallError as error:
- if error.status == HTTPStatus.NOT_FOUND:
- tag = session.Tags.new(
- name=tag_name, comment=tag_comment)
- else:
- raise
-
-# List all the tags.
-print(">>> Tags.list()")
-pprint(session.Tags.list())
-
-# Get the system IDs for all nodes.
-print(">>> Nodes.list()")
-all_nodes_system_ids = [
- node["system_id"] for node in session.Nodes.list()
-]
-pprint(all_nodes_system_ids)
-
-# Associate the tag with all nodes.
-print(">>> Tag.update_nodes()")
-pprint(session.Tag.update_nodes(
- name=tag["name"], add=all_nodes_system_ids))
-```
diff --git a/doc/client/events.md b/doc/client/events.md
new file mode 100644
index 00000000..f8d2e658
--- /dev/null
+++ b/doc/client/events.md
@@ -0,0 +1,34 @@
+Events
+
+Events are similar to other client objects... but a little different
+too. The only way to get events is by the ``query`` method:
+
+```pycon
+>>> events = client.events.query()
+```
+
+This accepts a plethora of optional arguments to narrow down the results:
+
+```pycon
+>>> events = client.events.query(hostnames={"foo", "bar"})
+>>> events = client.events.query(domains={"example.com", "maas.io"})
+>>> events = client.events.query(zones=["red", "blue"])
+>>> events = client.events.query(macs=("12:34:56:78:90:ab", ))
+>>> events = client.events.query(system_ids=…)
+>>> events = client.events.query(agent_name=…)
+>>> events = client.events.query(level=…)
+>>> events = client.events.query(after=…, limit=…)
+>>> events = client.events.query(owner=…)
+```
+
+These arguments can be combined to narrow the results even further.
+
+The ``level`` argument is a little special. It's a choice from a
+predefined set. For convenience, those choices are available in
+``client.events``:
+
+```pycon
+>>> events = client.events.query(level=client.events.ERROR)
+```
+
+but you can also pass in the string "ERROR" or the number 40.
diff --git a/doc/client/index.md b/doc/client/index.md
new file mode 100644
index 00000000..d6f6e9ad
--- /dev/null
+++ b/doc/client/index.md
@@ -0,0 +1,110 @@
+The Web API client
+
+Calling ``maas.client.connect`` or ``maas.client.login`` (MAAS 2.2+
+only) will return a ``maas.client.facade.Client`` instance. This
+provides an easy to understand starting point for working with MAAS's
+Web API.
+
+
+## An example
+
+```python
+#!/usr/bin/env python3.6
+
+import maas.client
+
+# Replace … with an API key previously obtained by hand from
+# http://$host:$port/MAAS/account/prefs/.
+client = maas.client.connect(
+ "http://localhost:5240/MAAS/", apikey="…")
+
+# Get a reference to self.
+myself = client.users.whoami()
+assert myself.is_admin, "%s is not an admin" % myself.username
+
+# Check for a MAAS server capability.
+version = client.version.get()
+assert "devices-management" in version.capabilities
+
+# Check the default OS and distro series for deployments.
+print(client.maas.get_default_os())
+print(client.maas.get_default_distro_series())
+
+# Set the HTTP proxy.
+client.maas.set_http_proxy("http://localhost:3128")
+
+# Allocate and deploy a machine.
+machine = client.machines.allocate()
+machine.deploy()
+```
+
+
+### Using `login`
+
+Alternatively, a client can be obtained from a username and password,
+replacing the call to `connect` above. This only works in MAAS 2.2 and
+above; below that a `LoginNotSupported` exception will be raised.
+
+```python
+client = login(
+ "http://localhost:5240/MAAS/",
+ username="foo", password="bar",
+)
+```
+
+
+### Again, but asynchronous
+
+At first glance _python-libmaas_ appears to be a blocking API, but it's
+actually asynchronous under the skin, based on [asyncio][]. If you call
+into _python-libmaas_ from within a running event loop it will behave
+asynchronously, but called from outside it behaves synchronously, and
+blocks.
+
+Using _python-libmaas_ interactively, when exploring the library or
+trying something out, is familiar and natural because it behaves as a
+synchronous, blocking API. This mode can be used of in scripts too, but
+the same code can be easily repurposed for use in an asynchronous,
+non-blocking application.
+
+Below shows the earlier example but implemented in an asynchronous
+style. Note the use of the ``asynchronous`` decorator: this is used
+heavily in _python-libmaas_ — along with the ``Asynchronous`` metaclass
+— to create the automatic blocking/not-blocking behaviour.
+
+```python
+#!/usr/bin/env python3.6
+
+from maas.client import login
+from maas.client.utils.async import asynchronous
+
+@asynchronous
+async def work_with_maas():
+ client = await login(
+ "http://eucula.local:5240/MAAS/",
+ username="gavin", password="f00b4r")
+
+ # Get a reference to self.
+ myself = await client.users.whoami()
+ assert myself.is_admin, "%s is not an admin" % myself.username
+
+ # Check for a MAAS server capability.
+ version = await client.version.get()
+ assert "devices-management" in version.capabilities
+
+ # Check the default OS and distro series for deployments.
+ print(await client.maas.get_default_os())
+ print(await client.maas.get_default_distro_series())
+
+ # Set the HTTP proxy.
+ await client.maas.set_http_proxy("http://localhost:3128")
+
+ # Allocate and deploy a machine.
+ machine = await client.machines.allocate()
+ await machine.deploy()
+
+work_with_maas()
+```
+
+
+[asyncio]: https://docs.python.org/3/library/asyncio.html
diff --git a/doc/client/interfaces.md b/doc/client/interfaces.md
new file mode 100644
index 00000000..21219487
--- /dev/null
+++ b/doc/client/interfaces.md
@@ -0,0 +1,204 @@
+Interfaces
+
+Given an ``Node`` instance bound to your MAAS server, you can
+view and modify its interface configuration. This applies to all ``Machine``,
+``Device``, ``RackController``, and ``RegionController``.
+
+## Read interfaces
+
+All ``Node`` objects have an ``interfaces`` property that provide a sequence of
+all ``Interface``'s on the ``Node``.
+
+```pycon
+>>> machine.interfaces
+>]>
+>>> machine.boot_interface
+>
+```
+
+On bond, VLAN, and bridge interfaces you can get the parents that make the
+interface. You can also go the other direction and view the children interfaces
+that are using this interface.
+
+**Note:** Parent and children objects are unloaded so they must be loaded to
+access the properties of the object.
+
+```pycon
+>>> bond.parents
+ (unloaded)>,
+ (unloaded)>,
+ ]>
+>>> ens3 = bond.parents[0]
+>>> ens3.loaded
+False
+>>> ens3.refresh()
+>>> ens3.type
+
+>>> ens3.children
+ (unloaded)>,
+ ]>
+```
+
+## Get interface by name
+
+The ``interfaces`` property on ``Node`` gives you access to all interfaces on
+the node. Sometimes you want to access the interface objects by name.
+``by_name`` and ``get_by_name`` are helpers on ``Interfaces`` that help.
+
+```pycon
+>>> machine.interfaces.by_name
+{'bond0': >,
+ 'ens3': >,
+ 'ens8': >}
+>>> bond = machine.interfaces.get_by_name('bond0')
+>>> bond
+>
+```
+
+## Read IP configuration
+
+Every ``Interface`` has a ``links`` property that provides all the IP
+information on how the interface is configured.
+
+```pycon
+>>> bond.links
+
+ subnet=>>]>
+```
+
+## Create physical
+
+Creation of interfaces is done directly on the ``interfaces`` property of a
+``Node``. Physical interface is the default type for the ``create`` method so
+only ``mac_address`` is required.
+
+```pycon
+>>> new_phy = machine.interfaces.create(mac_address="00:11:22:aa:bb:cc")
+>>> new_phy
+>
+```
+
+By default the interface is created disconnected. To create it the interface
+with it connected to a VLAN pass the ``vlan`` parameter.
+
+```pycon
+>>> default_vlan = client.fabrics.get_default().vlans.get_default()
+>>> new_phy = machine.interfaces.create(
+... mac_address="00:11:22:aa:bb:cc", vlan=default_vlan)
+>>> new_phy
+>
+>>> new_phy.vlan
+
+```
+
+## Create bond
+
+Bond creation is the same as creating a physical interface but an
+``InterfaceType`` is provided with options specific for a bond.
+
+```pycon
+>>> new_bond = machine.interfaces.create(
+... InterfaceType.BOND, name='bond0', parents=machine.interfaces,
+... bond_mode='802.3ad')
+>>> new_bond
+>
+>>> new_bond.params
+{'bond_downdelay': 0,
+ 'bond_lacp_rate': 'slow',
+ 'bond_miimon': 100,
+ 'bond_mode': '802.3ad',
+ 'bond_updelay': 0,
+ 'bond_xmit_hash_policy': 'layer2'}
+```
+
+## Create vlan
+
+VLAN creation only requires a single parent and a tagged VLAN to connect
+the interface to.
+
+```pycon
+>>> default_fabric = client.fabrics.get_default()
+>>> vlan_10 = default_fabric.vlans.create(10)
+>>> vlan_nic = machine.interfaces.create(
+... InterfaceType.VLAN, parent=new_bond, vlan=vlan_10)
+>>> vlan_nic
+>
+```
+
+## Create bridge
+
+Bridge creation only requires the name and parent interface you want the
+bridge to be created on.
+
+```pycon
+>>> bridge_nic = machine.interfaces.create(
+... InterfaceType.BRIDGE, name='br0', parent=vlan_nic)
+>>> bridge_nic
+>
+```
+
+## Update interface
+
+To update an interface just changing the properties of the interface and
+calling ``save`` is all that is required.
+
+```pycon
+>>> new_bond.name = 'my-bond'
+>>> new_bond.params['bond_mode'] = 'active-backup'
+>>> new_bond.save()
+```
+
+## Change IP configuration
+
+To adjust the IP configuration on a specific interface ``create`` on the
+``links`` property and ``delete`` on the ``InterfaceLink`` can be used.
+
+```pycon
+>>> new_bond.links.create(LinkMode.AUTO, subnet=subnet)
+
+ subnet=>>
+>>> new_bond.links[-1].delete()
+>>> new_bond.links.create(
+... LinkMode.STATIC, subnet=subnet, ip_address='192.168.122.1')
+
+ subnet=>>
+>>> new_bond.links[-1].delete()
+```
+
+## Disconnect interface
+
+To completely mark an interface as disconnected and remove all configuration
+the ``disconnect`` call makes this easy.
+
+```
+>>> new_bond.disconnect()
+```
+
+## Delete interface
+
+``delete`` exists directly on the ``Interface`` object so deletion is simple.
+
+```pycon
+>>> new_bond.delete()
+```
diff --git a/doc/client/networking.md b/doc/client/networking.md
new file mode 100644
index 00000000..d19a2520
--- /dev/null
+++ b/doc/client/networking.md
@@ -0,0 +1,130 @@
+Fabrics, VLANs, Subnets, Spaces, IP Ranges, Static Routes
+
+Given a ``Client`` instance bound to your MAAS server, you can
+interrogate your entire networking configuration.
+
+## Read networking
+
+``fabrics``, ``subnets``, ``spaces``, ``ip_ranges``, and ``static_routes`` is
+exposed directly on your ``Client`` instance. ``vlans`` are nested under each
+``Fabric``.
+
+```pycon
+>>> fabrics = client.fabrics.list()
+>>> len(fabrics)
+1
+>>> default_fabric = fabrics.get_default()
+>>> default_fabric.name
+'fabric-0'
+>>> default_fabric.vlans
+]>
+>>> for vlan in default_fabric.vlans:
+... print(vlan)
+...
+
+>>>
+```
+
+Get a specific subnet and view the ``Vlan`` and ``Fabric`` that it is
+assigned to. Going up the tree from ``Vlan`` to ``Fabric`` results in an
+unloaded ``Fabric``. Calling ``refresh`` on ``Fabric`` will load the object
+from MAAS.
+
+
+```pycon
+>>> vm_subnet = client.subnets.get('192.168.122.0/24')
+>>> vm_subnet.cidr
+'192.168.122.0/24'
+>>> vm_subnet.vlan
+
+>>> fabric = vm_subnet.vlan.fabric
+>>> fabric
+
+>>> fabric.refresh()
+>>> fabric.vlans
+Traceback (most recent call last):
+...
+ObjectNotLoaded: cannot access attribute 'vlans' of object 'Fabric'
+>>> fabric.is_loaded
+False
+>>> fabric.refresh()
+>>> fabric.is_loaded
+True
+>>> fabric.vlans
+]>
+```
+
+Access to ``spaces``, ``ip_ranges``, and ``static_routes`` works similarly.
+
+```pycon
+>>> client.spaces.list()
+>>> client.ip_ranges.list()
+>>> client.static_routes.list()
+```
+
+## Create fabric & vlan
+
+Creating a new fabric and vlan is done directly from each set of objects on
+the ``Client`` respectively.
+
+```pycon
+>>> new_fabric = client.fabrics.create()
+>>> new_fabric.name
+'fabric-2'
+>>> new_vlan = new_fabric.vlans.create(20)
+>>> new_vlan
+
+>>> new_vlan.fabric
+
+```
+
+## Create subnet
+
+Create a new subnet and assign it to an existing vlan.
+
+```pycon
+>>> new_subnet = client.subnets.create('192.168.128.0/24', new_vlan)
+>>> new_subnet.cidr
+'192.168.128.0/24'
+>>> new_subnet.vlan
+
+```
+
+## Update subnet
+
+Quickly move the newly created subnet from vlan to default fabric
+untagged vlan.
+
+```pycon
+>>> default_fabric = client.fabrics.get_default()
+>>> untagged = default_fabric.vlans.get_default()
+>>> new_subnet.vlan = untagged
+>>> new_subnet.save()
+>>> new_subnet.vlan
+
+```
+
+## Delete subnet
+
+``delete`` exists directly on the ``Subnet`` object so deletion is simple.
+
+```pycon
+>>> new_subnet.delete()
+>>>
+```
+
+## Enable DHCP
+
+Create a new dynamic IP range and turn DHCP on the selected
+rack controller.
+
+```pycon
+>>> fabric = client.fabrics.get_default()
+>>> untagged = fabric.vlans.get_default()
+>>> new_range = client.ip_ranges.create(
+... '192.168.122.100', '192.168.122.200', type=IPRangeType.DYNAMIC)
+>>> rack = client.rack_controllers.list()[0]
+>>> untagged.dhcp_on = True
+>>> untagged.primary_rack = rack
+>>> untagged.save()
+```
diff --git a/doc/client/nodes.md b/doc/client/nodes.md
new file mode 100644
index 00000000..2a3f308f
--- /dev/null
+++ b/doc/client/nodes.md
@@ -0,0 +1,296 @@
+Machines, devices, racks, and regions
+
+Given a ``Client`` instance bound to your MAAS server, you can
+interrogate your nodes.
+
+## Read nodes
+
+Each node type exists on the client: ``machines``, ``devices``,
+``rack_controllers``, ``region_controllers``.
+
+```pycon
+>>> client.machines.list()
+]>
+>>> client.devices.list()
+
+>>> client.rack_controllers.list()
+]>
+>>> client.region_controllers.list()
+]>
+```
+
+Easily iterate through the machines.
+
+```pycon
+>>> for machine in client.machines.list():
+... print(repr(machine))
+
+```
+
+Get a machine from its system_id.
+
+```pycon
+>>> machine = client.machines.get(system_id="pncys4")
+>>> machine
+
+```
+
+Machines — and devices, racks, and regions — have many useful
+attributes:
+
+```pycon
+>>> machine.architecture
+'amd64/generic'
+>>> machine.cpus
+4
+```
+
+Don't forget to try using tab-completion — the objects have been
+designed to be particularly friendly for interactive use — or
+``dir(machine)`` to find out what other fields and methods are
+available.
+
+## Create nodes
+
+Create a machine in MAAS. The architecture, MAC addresses, and power type are
+required fields.
+
+```pycon
+>>> machine = client.machines.create(
+... "amd64", ["00:11:22:33:44:55", "AA:BB:CC:DD:EE:FF"], "manual")
+
+```
+
+Normally you need to pass in power parameter so MAAS can talk to the BMC.
+
+```pycon
+>>> machine = client.machines.create(
+... "amd64", ["00:11:22:33:44:55", "AA:BB:CC:DD:EE:FF"], "ipmi", {
+... "power_address": "10.245.0.10",
+... "power_user": "root",
+... "power_pass": "calvin",
+... })
+>>> machine
+
+>>> machine.status
+
+```
+
+## Updating nodes
+
+Updating a machine is as simple as modifying the attribute and saving.
+
+```pycon
+>>> machine.hostname = 'my-machine'
+>>> machine.architecture = 'i386/generic'
+>>> machine.save()
+```
+
+## Deleting nodes
+
+Delete a machine is simple as calling delete on the machine object.
+
+```pycon
+>>> machine.delete()
+```
+
+## Assigning tags
+
+Assigning tags to a machine is as simple as calling `add` or `remove` on
+`tags` attribute.
+
+```pycon
+>>> new_tag = client.tags.create('new')
+>>> machine.tags.add(new_tag)
+>>> machine.tags
+]>
+>>> machine.tags.remove(new_tag)
+```
+
+## Commissioning and testing
+
+Easily commission a machine and wait until it successfully completes. By
+default the `commission` method waits until commissioning succeeds.
+
+```pycon
+>>> machine.commission()
+>>> machine.status
+NodeStatus.READY
+```
+
+A more advanced asyncio based script that runs commissioning with extra scripts
+and waits until all machines have successfully commissioned.
+
+```python
+#!/usr/bin/env python3
+
+import asyncio
+
+from maas.client import login
+from maas.client.enum import NodeStatus
+from maas.client.utils.async import asynchronous
+
+
+@asynchronous
+async def commission_all_machines():
+ client = await login(
+ "http://eucula.local:5240/MAAS/",
+ username="gavin", password="f00b4r")
+
+ # Get all machines that are in the NEW status.
+ all_machines = await client.machines.list()
+ new_machines = [
+ machine
+ for machine in all_machines
+ if machine.status == NodeStatus.NEW
+ ]
+
+ # Run commissioning with a custom commissioning script on all new machines.
+ for machine in new_machines:
+ machine.commission(
+ commissioning_scripts=['clear_hardware_raid'], wait=False)
+
+ # Wait until all machines are ready.
+ failed_machines = []
+ completed_machines = []
+ while len(new_machines) > 0:
+ await asyncio.sleep(5)
+ for machine in list(new_machines):
+ await machine.refresh()
+ if machine.status in [
+ NodeStatus.COMMISSIONING, NodeStatus.TESTING]:
+ # Machine is still commissioning or testing.
+ continue
+ elif machine.status == NodeStatus.READY:
+ # Machine is complete.
+ completed_machines.append(machine)
+ new_machines.remove(machine)
+ else:
+ # Machine has failed commissioning.
+ failed_machines.append(machine)
+ new_machines.remove(machine)
+
+ # Print message if any machines failed to commission.
+ if len(failed_machines) > 0:
+ for machine in failed_machines:
+ print("%s: transitioned to unexpected status - %s" % (
+ machine.hostname, machine.status_name))
+ else:
+ print("Successfully commissioned %d machines." % len(
+ completed_machines))
+
+
+commission_all_machines()
+```
+
+## Allocating and deploying
+
+```pycon
+>>> help(client.machines.allocate)
+Help on method allocate in module maas.client.viscera.machines:
+
+allocate(
+ *, hostname:str=None, architecture:str=None, cpus:int=None,
+ memory:float=None, tags:typing.Sequence=None)
+ method of maas.client.viscera.machines.MachinesType instance
+ Allocate a machine.
+
+ :param hostname: The hostname to match.
+ :param architecture: The architecture to match, e.g. "amd64".
+ :param cpus: The minimum number of CPUs to match.
+ :param memory: The minimum amount of RAM to match.
+ :param tags: The tags to match, as a sequence. Each tag may be
+ prefixed with a hyphen to denote that the given tag should NOT be
+ associated with a matched machine.
+>>> machine = client.machines.allocate(tags=("foo", "-bar"))
+>>> print(machine.status)
+NodeStatus.COMMISSIONING
+>>> machine.deploy()
+>>> print(machine.status)
+NodeStatus.DEPLOYING
+```
+
+## Abort
+
+If an action is performed on a machine and it needs to be aborted before it
+finishes ``abort`` can be used.
+
+```pycon
+>>> machine.commission(wait=False)
+>>> machine.status
+NodeStatus.COMMISSIONING
+>>> machine.abort()
+>>> machine.status
+NodeStatus.NEW
+```
+
+## Rescue mode
+
+Boot the machine into rescue mode and then exit.
+
+```pycon
+>>> machine.enter_rescue_mode()
+>>> machine.exit_rescue_mode()
+```
+
+## Broken & Fixed
+
+When a machine is identified as broken you can easily mark it broken and then
+fixed once the issue is resolved.
+
+```pycon
+>>> machine.mark_broken()
+>>> machine.status
+NodeStatus.BROKEN
+>>> machine.mark_fixed()
+>>> machine.status
+NodeStatus.READY
+```
+
+## Owner Data
+
+Owner data is extra information that you can set on a machine to hold some state information.
+
+**Note:** Once the machine is no longer in your control the information will be lost.
+
+```pycon
+>>> machine.owner_data
+{}
+>>> machine.owner_data['state'] = 'my-state-info'
+>>> machine.save()
+>>> machine.owner_data
+{'state': 'my-state-info'}
+>>> machine.release()
+>>> machine.owner_data
+{}
+```
+
+## Power Control
+
+The power state of a machine can be controlled outside of deploy, releasing, and rescue mode. If you need to control the power of a BMC independently the `power_on`, `power_off` and `query_power_state` can be of help.
+
+
+```pycon
+>>> machine.power_state
+PowerState.ON
+>>> machine.power_off()
+>>> machine.power_state
+PowerState.OFF
+>>> machine.power_on()
+>>> machine.power_state
+PowerState.ON
+>>> machine.query_power_state()
+PowerState.ON
+```
+
+## Reset Configuration
+
+It is possible to restore the machine back to exactly how it was after you completed commissioning. This is helpful when you have made a configuration that you no longer want or you want to start fresh.
+
+```pycon
+>>> machine.restore_default_configuration()
+>>> # Only restore networking.
+>>> machine.restore_networking_configuration()
+>>> # Only restore storage configuration.
+>>> machine.restore_storage_configuration()
+```
diff --git a/doc/viscera/other.md b/doc/client/other.md
similarity index 62%
rename from doc/viscera/other.md
rename to doc/client/other.md
index 12c96d55..c4ddc72b 100644
--- a/doc/viscera/other.md
+++ b/doc/client/other.md
@@ -1,4 +1,8 @@
-# Other objects
+Other objects
+
+There are several other object types available via the client API. Use
+``dir()`` and tab-completion to dig around interactively, or read the
+code; we've tried to keep it readable.
## Files, users, tags
@@ -6,9 +10,9 @@
Similarly to nodes, these sets of objects can be fetched:
```pycon
->>> tags = origin.Tags.read()
->>> files = origin.Files.read()
->>> users = origin.Users.read()
+>>> tags = client.tags.list()
+>>> files = client.files.list()
+>>> users = client.users.list()
```
When reading from collections, as above, the returned object is
diff --git a/doc/development/adding-an-object.md b/doc/development/adding-an-object.md
new file mode 100644
index 00000000..608c2068
--- /dev/null
+++ b/doc/development/adding-an-object.md
@@ -0,0 +1,286 @@
+Adding a new object type
+
+This will show the process by which we can add support for _Space_
+objects, but it should be roughly applicable to other objects.
+
+----
+
+
+## Skeleton
+
+Start by creating a new file in _viscera_. Following the example of
+existing objects, name it `maas/client/viscera/spaces.py` (i.e. plural).
+
+> Why _viscera_? The client we recommend for users is a façade of
+> _viscera_, allowing us to present a simplified interface which mingles
+> set-like operations with individual ones. This is friendlier to a new
+> developer, but _viscera_ itself keeps the two separate for cleanliness
+> of implementation.
+
+Create a skeleton for _Space_ and _Spaces_:
+
+```python
+"""Objects for spaces."""
+
+__all__ = [
+ "Space",
+ "Spaces",
+]
+
+from . import (
+ Object,
+ ObjectSet,
+ ObjectType,
+)
+
+
+class SpacesType(ObjectType):
+ """Metaclass for `Spaces`."""
+
+
+class Spaces(ObjectSet, metaclass=SpacesType):
+ """The set of spaces."""
+
+
+class SpaceType(ObjectType):
+ """Metaclass for `Space`."""
+
+
+class Space(Object, metaclass=SpaceType):
+ """A space."""
+```
+
+We create explicit type classes as a place to put class-specific
+information and methods. Most interestingly, methods created on the type
+classes are _class_ methods on instances of the type. For example:
+
+```pycon
+>>> class FooType(type):
+... def hello(cls):
+... return "Hello, %s" % cls
+
+>>> class Foo(metaclass=FooType):
+... def goodbye(self):
+... return "Goodbye, %s" % self
+
+>>> Foo.hello()
+"Hello, "
+
+>>> foo = Foo()
+>>> foo.goodbye()
+'Goodbye, <__main__.Foo object at ...>'
+```
+
+The difference between using `@classmethod` and this is that those class
+methods are not available on instances:
+
+```pycon
+
+>>> foo.hello()
+Traceback (most recent call last):
+...
+AttributeError: 'Foo' object has no attribute 'hello'
+```
+
+This keeps the namespace uncluttered, which is good for interactive,
+exploratory development, and it keeps code cleaner too: a class method
+**must** be called via the class.
+
+
+## Getting this into the default `Origin`
+
+In `maas/client/viscera/__init__.py` is the default `Origin` class. This
+loads object definitions, like those above, and *binds* them to a
+particular server. More about that later, but for now you need to add
+`".spaces"` to `Origin.__init__`:
+
+```diff
+ ".files",
+ ".maas",
+ ".machines",
++ ".spaces",
+ ".tags",
+ ".users",
+ ".version",
+```
+
+
+## Basic accessors
+
+Add the following basic accessor method to `SpacesType`:
+
+```python
+class SpacesType(ObjectType):
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+```
+
+Let's start working against a real MAAS server:
+
+```console
+$ bin/maas login my-server http://.../MAAS username p4ssw0rd
+$ bin/pip install -IU IPython # Don't leave home without it.
+$ bin/maas shell --viscera
+Welcome to the MAAS shell.
+
+Predefined objects:
+
+ client:
+ A pre-canned client for 'madagascar'.
+
+ origin:
+ A pre-canned `viscera` origin for 'madagascar'.
+```
+```pycon
+>>> origin.Spaces.read()
+, ]>
+
+>>> origin.Spaces._handler
+
+
+>>> origin.Spaces._origin
+
+```
+
+The `_handler` attribute is the _bones_ handler for spaces. We named the
+class "Spaces" and `Origin` paired that up with the _bones_ handler of
+the same name. This let us call the lower-level `read()` method. Try
+calling it now:
+
+```pycon
+>>> origin.Spaces._handler.read()
+[{'id': 0,
+ 'name': 'space-0',
+ 'resource_uri': '/MAAS/api/2.0/spaces/0/',
+ 'subnets': [],
+ 'vlans': []},
+ {'id': -1,
+ 'name': 'undefined',
+ 'resource_uri': '/MAAS/api/2.0/spaces/undefined/',
+ 'subnets': [{'active_discovery': False,
+ 'allow_proxy': True,
+ 'cidr': '192.168.1.0/24',
+ 'dns_servers': [],
+ 'gateway_ip': '192.168.1.254',
+ 'id': 1,
+ 'managed': True,
+ 'name': '192.168.1.0/24',
+ 'rdns_mode': 2,
+ 'resource_uri': '/MAAS/api/2.0/subnets/1/',
+ 'space': 'undefined',
+ 'vlan': {'dhcp_on': True,
+ 'external_dhcp': None,
+ 'fabric': 'fabric-0',
+ 'fabric_id': 0,
+ 'id': 5001,
+ 'mtu': 1500,
+ 'name': 'untagged',
+ 'primary_rack': '4y3h7n',
+ 'relay_vlan': None,
+ 'resource_uri': '/MAAS/api/2.0/vlans/5001/',
+ 'secondary_rack': 'xfaxgw',
+ 'space': 'undefined',
+ 'vid': 0}}],
+ 'vlans': [{'dhcp_on': True,
+ 'external_dhcp': None,
+ 'fabric': 'fabric-0',
+ 'fabric_id': 0,
+ 'id': 5001,
+ 'mtu': 1500,
+ 'name': 'untagged',
+ 'primary_rack': '4y3h7n',
+ 'relay_vlan': None,
+ 'resource_uri': '/MAAS/api/2.0/vlans/5001/',
+ 'secondary_rack': 'xfaxgw',
+ 'space': 'undefined',
+ 'vid': 0}]}]
+```
+
+Lots of information!
+
+> By the way, many or most of the IO methods in _python-libmaas_ can be
+> called interactively or in a script and they work the same as any
+> other synchronous or blocking call. Internally, however, they're all
+> asynchronous. They're wrapped in such a way that, when called from
+> outside of an _asyncio_ event-loop, they block, but inside they work
+> just the same as any other asynchronous call.
+
+Let's look at those `Space` objects:
+
+```pycon
+>>> space, *_ = origin.Spaces.read()
+
+>>> dir(space)
+[..., '_data', '_handler', '_origin']
+
+>>> space._data
+{'id': 0,
+ 'name': 'space-0',
+ 'resource_uri': '/MAAS/api/2.0/spaces/0/',
+ 'subnets': [],
+ 'vlans': []}
+
+>>> space._handler
+
+
+>>> space._origin is origin
+True
+```
+
+The handler has been associated with this object type like it was for
+`Spaces`, so now's a good time to add another accessor method:
+
+```python
+class SpaceType(ObjectType):
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(data)
+```
+
+Try it out:
+
+```pycon
+>>> space = origin.Space.read(0)
+
+>>> space._data
+{'id': 0,
+ 'name': 'space-0',
+ 'resource_uri': '/MAAS/api/2.0/spaces/0/',
+ 'subnets': [],
+ 'vlans': []}
+```
+
+
+## Getting at the data
+
+We don't want to work with that `_data` dictionary, we want attributes:
+
+```python
+class Space(Object, metaclass=SpaceType):
+ """A space."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True)
+ name = ObjectField.Checked("name", check(str), readonly=True)
+```
+
+Try it out in the shell:
+
+```pycon
+>>> space.id, space, name
+(0, 'space-0')
+```
+
+
+## Next steps
+
+That's enough for now, but there's plenty of ground yet to be covered:
+
+* How to work with the information about subnets and VLANs data that was
+ returned.
+
+* How to create, modify, and delete objects.
+
+* How to test all of this.
diff --git a/doc/development/releasing.md b/doc/development/releasing.md
new file mode 100644
index 00000000..559f48e3
--- /dev/null
+++ b/doc/development/releasing.md
@@ -0,0 +1,28 @@
+Releasing a new version of python-libmaas
+
+1. Clean and test:
+
+ make clean
+ make test
+
+1. If you didn't `make clean` just now, do it! Without it the [PyPI][]
+ uploads may be built incorrectly.
+
+1. Bump version in ``setup.py``, merge to _master_.
+
+1. Tag _master_:
+
+ git tag --sign ${version} --message "Release ${version}."
+ git push origin --tags
+
+1. Build and push docs to [GitHub][docs]:
+
+ make docs-to-github
+
+1. Build and push source and wheel to [PyPI][]:
+
+ make upload
+
+
+[docs]: http://maas.github.io/python-libmaas/
+[pypi]: https://pypi.python.org/pypi/python-libmaas
diff --git a/doc/index.md b/doc/index.md
index e2a2662a..5813d83e 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -1,41 +1,235 @@
-# Welcome to MAAS's new command-line tool & Python client libraries.
+Welcome to MAAS's new command-line tool & Python client library
-For documentation on the MAAS server components, visit
-[maas.ubuntu.com](https://maas.ubuntu.com/docs/).
+_python-libmaas_ provides:
+
+* A command-line tool for working with MAAS servers.
+
+* A rich and stable Python client library for interacting with MAAS 2.0+
+ servers. This can be used in a synchronous/blocking mode, or an
+ asynchronous/non-blocking mode based on [asyncio][].
+
+* A lower-level Python client library, auto-generated to match the MAAS
+ server it's interacting with.
+
+For MAAS _server_ documentation, visit
+[docs.ubuntu.com](https://docs.ubuntu.com/maas/).
+
+----
+
+This is **ALPHA** software. We are converging on a finished product, but
+until we release a beta all APIs could change.
+
+----
+
+
+## Installation
+
+Either work from a branch:
+
+```console
+$ git clone https://github.com/canonical/python-libmaas.git
+$ cd python-libmaas
+$ make
+```
+
+Or install with [pip](https://pip.pypa.io/) into a
+[virtualenv](https://virtualenv.readthedocs.org/):
+
+```console
+$ virtualenv --python=python3 amc && source amc/bin/activate
+$ pip install git+https://github.com/canonical/python-libmaas.git
+```
+
+Or install from [PyPI](https://pypi.python.org/):
+
+```console
+$ virtualenv --python=python3 amc && source amc/bin/activate
+$ pip install python-libmaas
+```
+
+**Note** that PyPI may lag the others.
+
+This documentation assumes you're working from a branch or in a
+virtualenv. In practice this means it will use partially qualified paths
+like ``bin/maas`` instead of bare ``maas`` invocations. If you've
+installed from PyPI the ``maas`` command will probably be installed on
+your shell's ``PATH`` so you can invoke it as ``maas``.
## Command-line
+Best place to start with the CLI is the help menu.
+
+```console
+$ bin/maas help
+$ bin/maas help commands
+```
+
+Once your have familiarized yourself with the available commands you will
+want to login to your MAAS. You can either pass arguments to login or it
+will ask your for the needed information to login.
+
+```console
+$ bin/maas login
+```
+
+The CLI supports multiple profiles with ``login``. Use ``profiles`` and
+``switch`` to view and change between profiles.
+
+```console
+$ bin/maas profiles
+┌─────────┬─────────────────────────────────────┬────────┐
+│ Profile │ URL │ Active │
+├─────────┼─────────────────────────────────────┼────────┤
+│ admin │ http://localhost:5240/MAAS/api/2.0/ │ ✓ │
+│ other │ http://localhost:5240/MAAS/api/2.0/ │ │
+└─────────┴─────────────────────────────────────┴────────┘
+$ bin/maas switch other
+$ bin/maas profiles
+┌─────────┬─────────────────────────────────────┬────────┐
+│ Profile │ URL │ Active │
+├─────────┼─────────────────────────────────────┼────────┤
+│ admin │ http://localhost:5240/MAAS/api/2.0/ │ │
+│ other │ http://localhost:5240/MAAS/api/2.0/ │ ✓ │
+└─────────┴─────────────────────────────────────┴────────┘
+```
+
+The ``nodes``, ``machines``, ``devices``, and ``controllers`` provide access
+to either all nodes with ``nodes`` or specific node types with ``machines``,
+``devices``, and ``controllers``.
+
+```console
+$ bin/maas nodes
+┌────────────────────┬───────────────┐
+│ Hostname │ Type │
+├────────────────────┼───────────────┤
+│ another │ Device │
+│ blake-ubnt-desktop │ Regiond+rackd │
+│ testing │ Device │
+│ win2016 │ Machine │
+└────────────────────┴───────────────┘
+$ bin/maas machines
+┌──────────┬───────┬────────┬───────┬───────┬────────┐
+│ Hostname │ Power │ Status │ Arch │ #CPUs │ RAM │
+├──────────┼───────┼────────┼───────┼───────┼────────┤
+│ win2016 │ Off │ Broken │ amd64 │ 4 │ 8.0 GB │
+└──────────┴───────┴────────┴───────┴───────┴────────┘
+$ bin/maas devices
+┌──────────┬───────────────┐
+│ Hostname │ IP addresses │
+├──────────┼───────────────┤
+│ another │ 192.168.1.223 │
+│ testing │ 192.168.1.150 │
+│ │ 192.168.1.143 │
+└──────────┴───────────────┘
+$ bin/maas controllers
+┌────────────────────┬───────────────┬───────┬───────┬─────────┐
+│ Hostname │ Type │ Arch │ #CPUs │ RAM │
+├────────────────────┼───────────────┼───────┼───────┼─────────┤
+│ blake-ubnt-desktop │ Regiond+rackd │ amd64 │ 8 │ 24.0 GB │
+└────────────────────┴───────────────┴───────┴───────┴─────────┘
+```
+
+Tab-completion in ``bash`` and ``tcsh`` is supported too. For example,
+in ``bash``:
+
```console
-$ bin/maas profiles login --help
-$ bin/maas profiles login exmpl http://example.com:5240/MAAS/ my_username
-Password: …
-$ bin/maas list nodes
-┌───────────────┬───────────┬───────────────┬───────┬────────┬───────────┬───────┐
-│ Hostname │ System ID │ Architecture │ #CPUs │ RAM │ Status │ Power │
-├───────────────┼───────────┼───────────────┼───────┼────────┼───────────┼───────┤
-│ botswana.maas │ 433334 │ amd64/generic │ 4 │ 8.0 GB │ Ready │ Off │
-│ namibia.maas │ 433333 │ amd64/generic │ 4 │ 8.0 GB │ Allocated │ Off │
-└───────────────┴───────────┴───────────────┴───────┴────────┴───────────┴───────┘
+$ source <(bin/register-python-argcomplete --shell=bash bin/maas)
+$ bin/maas
+allocate files login nodes shell ...
+```
+
+
+## Client library
+
+For a developer the simplest entry points into ``python-libmaas`` are
+the ``connect`` and ``login`` functions in ``maas.client``. The former
+connects to a MAAS server using a previously obtained API key, and the
+latter logs-in to MAAS with your username and password. These returns a
+``Client`` object that has convenient attributes for working with MAAS.
+
+For example, this prints out all interfaces on all machines:
+
+```python
+from maas.client import login
+client = login(
+ "http://localhost:5240/MAAS/",
+ username="my_user", password="my_pass",
+)
+tmpl = "{0.hostname} {1.name} {1.mac_address}"
+for machine in client.machines.list():
+ for interface in machine.interfaces:
+ print(tmpl.format(machine, interface))
```
+Learn more about the [client](client/index.md).
+
+
+## Shell
+
+There's an interactive shell. If a profile name is given or a default
+profile has been set — see ``maas profiles --help`` — this places a
+``Client`` instance in the default namespace (as ``client``) that you
+can use interactively or in a script.
-## Client libraries
+For the best experience install [IPython](https://ipython.org/) first.
-There are two client libraries that make use of MAAS's Web API:
+```console
+$ bin/maas shell
+Welcome to the MAAS shell.
+...
+```
+
+```pycon
+>>> origin.Version.read()
+
+>>> dir(client)
+[..., 'account', 'boot_resources', ...]
+```
+
+Scripts can also be run. For example, given the following ``script.py``:
+
+```python
+print("Machines:", len(client.machines.list()))
+print("Devices:", len(client.devices.list()))
+print("Racks:", len(client.rack_controllers.list()))
+print("Regions:", len(client.region_controllers.list()))
+```
+
+the following will run it against the default profile:
+
+```console
+$ bin/maas shell script.py
+Machines: 1
+Devices: 0
+Racks: 2
+Regions: 1
+```
+
+
+## Development
+
+It's easy to start hacking on _python-libmaas_:
+
+```console
+$ git clone git@github.com:maas/python-libmaas.git
+$ cd python-libmaas
+$ make develop
+$ make test
+```
+
+Installing [IPython][] is generally a good idea too:
+
+```console
+$ bin/pip install -UI IPython
+```
-* A lower-level library that closely mirrors MAAS's Web API, referred to
- as _bones_. The MAAS server publishes a description of its Web API and
- _bones_ provides a convenient mechanism to interact with it.
+Pull requests are welcome but authors need to sign the [Canonical
+contributor license agreement][CCLA] before those PRs can be merged.
-* A higher-level library that's designed for developers, referred to as
- _viscera_. MAAS's Web API is sometimes unfriendly or inconsistent, but
- _viscera_ presents a saner API, designed for developers rather than
- machines.
-The implementation of [_viscera_](viscera/index.md) makes use of
-[_bones_](bones/index.md). _Viscera_ is the API that should be preferred
-for application development.
+[asyncio]: https://docs.python.org/3/library/asyncio.html
+[CCLA]: https://www.ubuntu.com/legal/contributors
-Try this next: [Get started with _viscera_](viscera/getting-started.md)
+[IPython]: https://ipython.org/
diff --git a/doc/viscera/events.md b/doc/viscera/events.md
deleted file mode 100644
index e7e8c354..00000000
--- a/doc/viscera/events.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Events
-
-Events are similar... but different. The only way to get events is by
-the ``query`` method:
-
-```pycon
->>> events = origin.Events.query()
-```
-
-This accepts a plethora of optional arguments to narrow down the results:
-
-```pycon
->>> events = origin.Events.query(hostnames={"foo", "bar"})
->>> events = origin.Events.query(domains={"example.com", "maas.io"})
->>> events = origin.Events.query(zones=["red", "blue"])
->>> events = origin.Events.query(macs=("12:34:56:78:90:ab", ))
->>> events = origin.Events.query(system_ids=…)
->>> events = origin.Events.query(agent_name=…)
->>> events = origin.Events.query(level=…)
->>> events = origin.Events.query(after=…, limit=…)
-```
-
-These arguments can be combined to narrow the results even further.
-
-The ``level`` argument is a little special. It's a choice from a
-predefined set. For convenience, those choices are defined in the
-``Level`` enum:
-
-```pycon
->>> events = origin.Events.query(level=origin.Events.Level.ERROR)
-```
-
-but you can also pass in the string "ERROR" or the number 40.
diff --git a/doc/viscera/getting-started.md b/doc/viscera/getting-started.md
deleted file mode 100644
index 8a588b2f..00000000
--- a/doc/viscera/getting-started.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# Getting started with _viscera_
-
-
-## Installation
-
-Either work from a branch:
-
-```console
-$ git clone https://github.com/maas/python-libmaas.git
-$ cd python-libmaas
-$ make
-```
-
-Or install with [pip](https://pip.pypa.io/) into a
-[virtualenv](https://virtualenv.readthedocs.org/):
-
-```console
-$ virtualenv --python=python3.5 amc && source amc/bin/activate
-$ pip install git+https://github.com/maas/python-libmaas.git
-```
-
-Or install from [PyPI](https://pypi.python.org/):
-
-```console
-$ virtualenv --python=python3.5 amc && source amc/bin/activate
-$ pip install python-libmaas
-```
-
-*Note* that PyPI may lag the others.
-
-
-## Logging-in
-
-Log-in using the command-line tool and start an interactive Python
-shell:
-
-```console
-$ maas profiles login foo http://example.com:5240/MAAS/ admin
-Password: …
-$ maas shell
-```
-
-This will provide you with a pre-prepared `origin` object that points to
-`foo` from above. This is the root object of the API.
-
-You can also log-in programmatically:
-
-```pycon
->>> profile, origin = Origin.login(
-... "http://example.com:5240/MAAS/", username="admin",
-... password="…")
-```
-
-The `profile` has not been saved, but it's easy to do so:
-
-```pycon
->>> profile = profile.replace(name="foo")
->>> with ProfileStore.open() as store:
-... store.save(profile)
-... store.default = profile
-```
-
-This does the same as the `maas profiles login` command.
-
-But there's no need! There's a command built in to do it for you:
-
-```console
-$ bin/maas shell
-Welcome to the MAAS shell.
-
-Predefined variables:
-
- origin: A `viscera` origin, configured for foo.
- session: A `bones` session, configured for foo.
-
->>>
-```
-
-
-## Logging-out
-
-Log-out using the command-line tool:
-
-```console
-$ bin/maas profiles remove foo
-```
-
-or, programmatically:
-
-```pycon
->>> with ProfileStore.open() as store:
-... store.delete("foo")
-```
-
-
-## `dir()`, `help()`, and tab-completion
-
-The _viscera_ API has been designed to be very discoverable using
-tab-completion, `dir()`, `help()`, and so on. Start with that:
-
-```pycon
->>> origin.
-…
-```
-
-This works best when you've got [IPython](https://ipython.org/)
-installed.
diff --git a/doc/viscera/index.md b/doc/viscera/index.md
deleted file mode 100644
index feff5044..00000000
--- a/doc/viscera/index.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# _Viscera_: High-level Python client API
-
-
-## Some example code
-
-```python
-#!/usr/bin/env python3.5
-
-from pprint import pprint
-
-from maas.client import viscera
-
-
-profile, origin = viscera.Origin.login(
- "http://localhost:5240/MAAS/", username="alice",
- password="wonderland")
-
-# List all the tags.
-print(">>> origin.Tags.read()")
-pprint(origin.Tags.read())
-print(">>> Or: list(origin.Tags)")
-pprint(list(origin.Tags))
-
-# List all the nodes.
-print(">>> origin.Nodes.read()")
-pprint(origin.Nodes.read())
-print(">>> Or: list(origin.Nodes)")
-pprint(list(origin.Nodes))
-```
diff --git a/doc/viscera/nodes.md b/doc/viscera/nodes.md
deleted file mode 100644
index 94f90aff..00000000
--- a/doc/viscera/nodes.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# _Viscera_: Working with nodes
-
-
-## Listing
-
-```pycon
->>> for node in origin.Nodes:
-... print(repr(node))
-
-
-```
-
-Individual nodes can be read from the Web API.
-
-```pycon
->>> node = origin.Node.read(system_id="433333")
-```
-
-Nodes have many useful attributes:
-
-```pycon
->>> node.architecture
-'amd64/generic'
->>> node.cpus
-4
-```
-
-Don't forget to try using tab-completion — the objects have been
-designed to be particularly friendly for interactive use — or
-``dir(node)`` to find out what other fields and methods are available.
-
-__TODO__: Updating nodes.
-
-
-## Acquiring and starting
-
-```pycon
->>> help(origin.Nodes.acquire)
-acquire(*, hostname:str=None, architecture:str=None, cpus:int=None,
- memory:float=None, tags:typing.Sequence=None) method of
- maas.client.viscera.NodesType instance
- :param hostname: The hostname to match.
- :param architecture: The architecture to match, e.g. "amd64".
- :param cpus: The minimum number of CPUs to match.
- :param memory: The minimum amount of RAM to match.
- :param tags: The tags to match, as a sequence. Each tag may be
- prefixed with a hyphen to denote that the given tag should NOT be
- associated with a matched node.
->>> node = origin.Nodes.acquire(tags=("foo", "-bar"))
->>> print(node.status_name)
-Acquired
->>> node.start()
->>> print(node.status_name)
-Deploying
-```
diff --git a/integrate/__init__.py b/integrate/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/integrate/__main__.py b/integrate/__main__.py
new file mode 100644
index 00000000..a386448e
--- /dev/null
+++ b/integrate/__main__.py
@@ -0,0 +1,7 @@
+import sys
+
+import testtools.run
+
+
+argv = sys.argv[0], "discover", "integrate", *sys.argv[1:]
+testtools.run.main(argv, sys.stdout)
diff --git a/integrate/test.py b/integrate/test.py
new file mode 100644
index 00000000..13d57b4d
--- /dev/null
+++ b/integrate/test.py
@@ -0,0 +1,311 @@
+"""Integration tests for `maas.client`."""
+
+from collections import Mapping
+from datetime import datetime
+from http import HTTPStatus
+import io
+from itertools import repeat
+import random
+from time import sleep
+
+from maas.client import (
+ bones,
+ viscera,
+)
+from maas.client.testing import (
+ make_name_without_spaces,
+ TestCase,
+)
+from maas.client.utils import (
+ creds,
+ profiles,
+ retries,
+)
+from testtools.matchers import (
+ AllMatch,
+ Equals,
+ Is,
+ IsInstance,
+ MatchesAll,
+ MatchesAny,
+ MatchesStructure,
+)
+
+
+kiB = 2 ** 10
+MiB = 2 ** 20
+
+
+def scenarios():
+ with profiles.ProfileStore.open() as config:
+ return tuple(
+ (profile.name, dict(profile=profile))
+ for profile in map(config.load, config)
+ )
+
+
+class IntegrationTestCase(TestCase):
+
+ scenarios = scenarios()
+
+ def setUp(self):
+ super(IntegrationTestCase, self).setUp()
+ self.session = bones.SessionAPI.fromProfile(self.profile)
+ self.origin = viscera.Origin(self.session)
+
+
+class TestAccount(IntegrationTestCase):
+
+ def test__create_and_delete_credentials(self):
+ credentials = self.origin.Account.create_credentials()
+ self.assertThat(credentials, IsInstance(creds.Credentials))
+ self.origin.Account.delete_credentials(credentials)
+
+
+class TestBootResources(IntegrationTestCase):
+
+ def test__list_boot_resources(self):
+ boot_resources = self.origin.BootResources.read()
+ self.assertThat(boot_resources, MatchesAll(
+ IsInstance(self.origin.BootResources),
+ AllMatch(IsInstance(self.origin.BootResource)),
+ ))
+ self.assertThat(
+ boot_resources,
+ AllMatch(MatchesStructure(
+ id=IsInstance(int),
+ type=IsInstance(str),
+ name=IsInstance(str),
+ architecture=IsInstance(str),
+ subarches=Optional(IsInstance(str)),
+ sets=Optional(IsInstance(Mapping)),
+ )),
+ )
+
+ def test__create_and_delete_boot_resource(self):
+ chunk = random.getrandbits(8 * 128).to_bytes(128, "big")
+ content = b"".join(repeat(chunk, 5 * MiB // len(chunk)))
+ boot_resource = self.origin.BootResources.create(
+ make_name_without_spaces("ubuntu", "/"), "amd64/generic",
+ io.BytesIO(content))
+ self.assertThat(boot_resource, IsInstance(self.origin.BootResource))
+ boot_resource.delete()
+ error = self.assertRaises(
+ bones.CallError, self.origin.BootResource.read, boot_resource.id)
+ self.assertThat(error, MatchesStructure(
+ status=Equals(HTTPStatus.NOT_FOUND)))
+
+
+class TestBootSources(IntegrationTestCase):
+
+ def test__create_and_delete_source_with_keyring_filename(self):
+ source_url = make_name_without_spaces("http://maas.example.com/")
+ keyring_filename = make_name_without_spaces("keyring-filename")
+ boot_source = self.origin.BootSources.create(
+ source_url, keyring_filename=keyring_filename)
+ self.assertThat(boot_source, IsInstance(self.origin.BootSource))
+ boot_source.delete()
+ error = self.assertRaises(
+ bones.CallError, self.origin.BootSource.read, boot_source.id)
+ self.assertThat(error, MatchesStructure(
+ status=Equals(HTTPStatus.NOT_FOUND)))
+
+ def test__create_and_delete_source_with_keyring_data(self):
+ source_url = make_name_without_spaces("http://maas.example.com/")
+ keyring_data = make_name_without_spaces("keyring-data").encode()
+ boot_source = self.origin.BootSources.create(
+ source_url, keyring_data=io.BytesIO(keyring_data))
+ self.assertThat(boot_source, IsInstance(self.origin.BootSource))
+ boot_source.delete()
+ error = self.assertRaises(
+ bones.CallError, self.origin.BootSource.read, boot_source.id)
+ self.assertThat(error, MatchesStructure(
+ status=Equals(HTTPStatus.NOT_FOUND)))
+
+ def test__list_boot_sources(self):
+ boot_sources = self.origin.BootSources.read()
+ self.assertThat(boot_sources, MatchesAll(
+ IsInstance(self.origin.BootSources),
+ AllMatch(IsInstance(self.origin.BootSource)),
+ ))
+ self.assertThat(
+ boot_sources,
+ AllMatch(MatchesStructure(
+ id=IsInstance(int),
+ url=IsInstance(str),
+ keyring_filename=IsInstance(str),
+ keyring_data=IsInstance(str), # ??? Binary, no?
+ created=IsInstance(datetime),
+ updated=IsInstance(datetime),
+ )),
+ )
+
+
+# TestBootSourceSelections
+# TestDevices
+
+
+class TestEvents(IntegrationTestCase):
+
+ def test__query_events(self):
+ events = self.origin.Events.query()
+ self.assertThat(events, IsInstance(self.origin.Events))
+ events = events.prev()
+ self.assertThat(events, IsInstance(self.origin.Events))
+ events = events.next()
+ self.assertThat(events, IsInstance(self.origin.Events))
+
+ def test__events(self):
+ self.assertThat(
+ self.origin.Events.query(),
+ AllMatch(MatchesStructure(
+ event_id=IsInstance(int),
+ event_type=IsInstance(str),
+ system_id=IsInstance(str),
+ hostname=IsInstance(str),
+ level=IsInstance(viscera.events.Level),
+ created=IsInstance(datetime),
+ description=IsInstance(str),
+ )),
+ )
+
+
+# TestFiles
+# TestMAAS
+
+
+class TestMachines(IntegrationTestCase):
+
+ def test__list_machines(self):
+ machines = self.origin.Machines.read()
+ self.assertThat(machines, MatchesAll(
+ IsInstance(self.origin.Machines),
+ AllMatch(IsInstance(self.origin.Machine)),
+ ))
+ self.assertThat(
+ machines,
+ AllMatch(MatchesStructure(
+ # This is NOT exhaustive.
+ system_id=IsInstance(str),
+ architecture=IsInstance(str),
+ hostname=IsInstance(str),
+ ip_addresses=IsInstance(list),
+ status=IsInstance(int),
+ status_name=IsInstance(str),
+ tags=IsInstance(list),
+ )),
+ )
+
+ def XXXtest__allocate_deploy_and_release(self):
+ machines_ready = [
+ machine for machine in self.origin.Machines.read()
+ if machine.status_name == "Ready"
+ ]
+ if len(machines_ready) == 0:
+ self.skip("No machines available.")
+
+ # Allocate one of the ready machines. XXX: This ought to be a method
+ # on Machine or take a `system_id` argument.
+ machine = random.choice(machines_ready)
+ machine = self.origin.Machines.allocate(hostname=machine.hostname)
+ self.assertThat(machine.status_name, Equals("Allocated"))
+
+ try:
+ # Deploy the machine with defaults.
+ machine = machine.deploy()
+ self.assertThat(machine.status_name, Equals("Deploying"))
+ # Wait for the machine to deploy.
+ for elapsed, remaining, wait in retries(600, 10):
+ machine = self.origin.Machine.read(machine.system_id)
+ if machine.status_name == "Deploying":
+ sleep(wait)
+ else:
+ break
+ else:
+ self.fail("Timed-out waiting for machine to deploy.")
+ # The machine has deployed.
+ self.assertThat(machine.status_name, Equals("Deployed"))
+
+ finally:
+ # Release the machine.
+ machine = machine.release("Finished with this now, thanks.")
+ self.assertThat(machine.status_name, Equals("Releasing"))
+ # Wait for the machine to release.
+ for elapsed, remaining, wait in retries(300, 10):
+ machine = self.origin.Machine.read(machine.system_id)
+ if machine.status_name == "Releasing":
+ sleep(wait)
+ else:
+ break
+ else:
+ self.fail("Timed-out waiting for machine to release.")
+ # The machine has been released.
+ self.assertThat(machine.status_name, Equals("Ready"))
+
+
+class TestRackControllers(IntegrationTestCase):
+
+ def test__list_rack_controllers(self):
+ machines = self.origin.RackControllers.read()
+ self.assertThat(machines, MatchesAll(
+ IsInstance(self.origin.RackControllers),
+ AllMatch(IsInstance(self.origin.RackController)),
+ ))
+ self.assertThat(
+ machines,
+ AllMatch(MatchesStructure(
+ # This is NOT exhaustive.
+ architecture=IsInstance(str),
+ cpus=IsInstance(int),
+ distro_series=IsInstance(str),
+ fqdn=IsInstance(str),
+ hostname=IsInstance(str),
+ ip_addresses=IsInstance(list),
+ memory=IsInstance(int),
+ power_state=IsInstance(str),
+ system_id=IsInstance(str),
+ zone=IsInstance(self.origin.Zone),
+ )),
+ )
+
+
+class TestRegionControllers(IntegrationTestCase):
+
+ def test__list_region_controllers(self):
+ machines = self.origin.RegionControllers.read()
+ self.assertThat(machines, MatchesAll(
+ IsInstance(self.origin.RegionControllers),
+ AllMatch(IsInstance(self.origin.RegionController)),
+ ))
+ self.assertThat(
+ machines,
+ AllMatch(MatchesStructure(
+ # This is NOT exhaustive.
+ architecture=IsInstance(str),
+ cpus=IsInstance(int),
+ distro_series=IsInstance(str),
+ fqdn=IsInstance(str),
+ hostname=IsInstance(str),
+ ip_addresses=IsInstance(list),
+ memory=IsInstance(int),
+ power_state=IsInstance(str),
+ system_id=IsInstance(str),
+ zone=IsInstance(self.origin.Zone),
+ )),
+ )
+
+
+# TestTags
+# TestTesting
+# TestUsers
+# TestVersion
+# TestZones
+
+
+# Additional matchers.
+
+def Optional(matcher, default=Is(None)):
+ return MatchesAny(matcher, default)
+
+
+# End.
diff --git a/maas/__init__.py b/maas/__init__.py
index ece379ce..5284146e 100644
--- a/maas/__init__.py
+++ b/maas/__init__.py
@@ -1,2 +1 @@
-import pkg_resources
-pkg_resources.declare_namespace(__name__)
+__import__("pkg_resources").declare_namespace(__name__)
diff --git a/maas/client/__init__.py b/maas/client/__init__.py
index 55532e15..874a8230 100644
--- a/maas/client/__init__.py
+++ b/maas/client/__init__.py
@@ -1,9 +1,12 @@
"""Basic entry points."""
-from . import _client
+__all__ = ["connect", "login"]
+from .utils.maas_async import asynchronous
-def connect(url, *, apikey=None, insecure=False):
+
+@asynchronous
+async def connect(url, *, apikey=None, insecure=False):
"""Connect to MAAS at `url` using a previously obtained API key.
:param url: The URL of MAAS, e.g. http://maas.example.com:5240/MAAS/
@@ -13,13 +16,15 @@ def connect(url, *, apikey=None, insecure=False):
:return: A client object.
"""
+ from .facade import Client # Lazy.
from .viscera import Origin # Lazy.
- profile, origin = Origin.connect(
- url, apikey=apikey, insecure=insecure)
- return _client.Client(origin)
+
+ profile, origin = await Origin.connect(url, apikey=apikey, insecure=insecure)
+ return Client(origin)
-def login(url, *, username=None, password=None, insecure=False):
+@asynchronous
+async def login(url, *, username=None, password=None, insecure=False):
"""Connect to MAAS at `url` with a user name and password.
:param url: The URL of MAAS, e.g. http://maas.example.com:5240/MAAS/
@@ -29,7 +34,10 @@ def login(url, *, username=None, password=None, insecure=False):
:return: A client object.
"""
+ from .facade import Client # Lazy.
from .viscera import Origin # Lazy.
- profile, origin = Origin.login(
- url, username=username, password=password, insecure=insecure)
- return _client.Client(origin)
+
+ profile, origin = await Origin.login(
+ url, username=username, password=password, insecure=insecure
+ )
+ return Client(origin)
diff --git a/maas/client/_client.py b/maas/client/_client.py
deleted file mode 100644
index 8b84340e..00000000
--- a/maas/client/_client.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""Client facade."""
-
-from functools import update_wrapper
-
-
-class Facade:
- """Present a simplified API for interacting with MAAS.
-
- The viscera API separates set-based interactions from those on individual
- objects — e.g. Machines and Machine — which mirrors the way MAAS's API is
- actually constructed, helps to avoid namespace clashes, and makes testing
- cleaner.
-
- However, we want to present a simplified commingled namespace to users of
- MAAS's *client* API. For example, all entry points related to machines
- should be available as ``client.machines``. This facade class allows us to
- present that commingled namespace without coding it as such.
- """
-
- def __init__(self, client, name, methods):
- super(Facade, self).__init__()
- self._client = client
- self._name = name
- self._populate(methods)
-
- def _populate(self, methods):
- for name, func in methods.items():
- setattr(self, name, func)
-
- def __repr__(self):
- return "<%s>" % self._name
-
-
-class FacadeDescriptor:
- """Lazily create a facade on first use.
-
- It will be stored in the instance dictionary using the given name. This
- should match the name by which the descriptor is bound into the instance
- class's namespace: as this is a non-data descriptor [1] this will yield
- create-on-first-use behaviour.
-
- The factory function should accept a single argument, an `Origin`, and
- return a dict mapping method names to methods of objects obtained from the
- origin.
-
- [1] https://docs.python.org/3.5/howto/descriptor.html#descriptor-protocol
- """
-
- def __init__(self, name, factory):
- super(FacadeDescriptor, self).__init__()
- self.name, self.factory = name, factory
-
- def __get__(self, obj, typ=None):
- methods = self.factory(obj._origin)
- facade = Facade(obj, self.name, methods)
- obj.__dict__[self.name] = facade
- return facade
-
-
-def facade(factory):
- """Declare a method as a facade factory."""
- wrapper = FacadeDescriptor(factory.__name__, factory)
- return update_wrapper(wrapper, factory)
-
-
-class Client:
- """A simplified API for interacting with MAAS."""
-
- def __init__(self, origin):
- super(Client, self).__init__()
- self._origin = origin
-
- @facade
- def machines(origin):
- return {
- "allocate": origin.Machines.allocate,
- "get": origin.Machine.read,
- "list": origin.Machines.read,
- }
-
- @facade
- def devices(origin):
- return {
- "get": origin.Device.read,
- "list": origin.Devices.read,
- }
diff --git a/maas/client/bones/__init__.py b/maas/client/bones/__init__.py
index defc5b6c..7b5b8d31 100644
--- a/maas/client/bones/__init__.py
+++ b/maas/client/bones/__init__.py
@@ -4,27 +4,21 @@
hence the name "bones".
"""
-__all__ = [
- "CallError",
- "SessionAPI",
-]
-
-from collections import (
- Iterable,
- namedtuple,
-)
-from http import HTTPStatus
+__all__ = ["CallError", "SessionAPI"]
+
+import typing
+
+from collections import namedtuple
+from collections.abc import Iterable
import json
-import re
-from urllib.parse import urljoin
+from urllib.parse import urlparse
import aiohttp
-import aiohttp.errors
+from . import helpers
from .. import utils
from ..utils import profiles
-from ..utils.connect import connect
-from ..utils.login import login
+from ..utils.maas_async import asynchronous
class SessionError(Exception):
@@ -35,26 +29,19 @@ class SessionAPI:
"""Represents an API session with a remote MAAS installation."""
@classmethod
- async def fromURL(
- cls, url, *, credentials=None, insecure=False):
+ @asynchronous
+ async def fromURL(cls, url, *, credentials=None, insecure=False):
"""Return a `SessionAPI` for a given MAAS instance."""
- url_describe = urljoin(url, "describe/")
- connector = aiohttp.TCPConnector(verify_ssl=(not insecure))
- session = aiohttp.ClientSession(connector=connector)
- async with session, session.get(url_describe) as response:
- if response.status != HTTPStatus.OK:
- raise SessionError(
- "{0} -> {1.status} {1.reason}".format(
- url, response))
- elif response.content_type != "application/json":
- raise SessionError(
- "Expected application/json, got: %s"
- % response.content_type)
- else:
- description = await response.json()
- session = cls(description, credentials)
- session.insecure = insecure
- return session
+ try:
+ description = await helpers.fetch_api_description(url, insecure=insecure)
+ except helpers.RemoteError as error:
+ # For now just re-raise as SessionError.
+ raise SessionError(str(error))
+ else:
+ session = cls(url, description, credentials)
+ session.scheme = urlparse(url).scheme
+ session.insecure = insecure
+ return session
@classmethod
def fromProfile(cls, profile):
@@ -62,7 +49,10 @@ def fromProfile(cls, profile):
:see: `ProfileStore`.
"""
- return 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
@classmethod
def fromProfileName(cls, name):
@@ -74,46 +64,51 @@ def fromProfileName(cls, name):
return cls.fromProfile(config.load(name))
@classmethod
- def login(
- cls, url, *, username=None, password=None, insecure=False):
+ @asynchronous
+ async def login(cls, url, *, username=None, password=None, insecure=False):
"""Make a `SessionAPI` by logging-in with a username and password.
:return: A tuple of ``profile`` and ``session``, where the former is
an unsaved `Profile` instance, and the latter is a `SessionAPI`
instance made using the profile.
"""
- profile = login(
- url=url, username=username, password=password, insecure=insecure)
- session = cls(profile.description, profile.credentials)
+ profile = await helpers.login(
+ url=url, username=username, password=password, insecure=insecure
+ )
+ session = cls(url, profile.description, profile.credentials)
+ session.scheme = urlparse(url).scheme
session.insecure = insecure
return profile, session
@classmethod
- def connect(
- cls, url, *, apikey=None, insecure=False):
+ @asynchronous
+ async def connect(cls, url, *, apikey=None, insecure=False):
"""Make a `SessionAPI` by connecting with an apikey.
:return: A tuple of ``profile`` and ``session``, where the former is
an unsaved `Profile` instance, and the latter is a `SessionAPI`
instance made using the profile.
"""
- profile = connect(
- url=url, apikey=apikey, insecure=insecure)
- session = cls(profile.description, profile.credentials)
+ profile = await helpers.connect(url=url, apikey=apikey, insecure=insecure)
+ session = cls(url, profile.description, profile.credentials)
+ session.scheme = urlparse(url).scheme
session.insecure = insecure
return profile, session
# Set these on instances.
+ scheme = "http"
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()
@@ -123,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
@@ -161,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
@@ -171,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
@@ -185,12 +182,7 @@ def __populate(self):
@property
def name(self):
"""A stable, human-readable name and identifier for this handler."""
- name = self.__handler["name"]
- if name.startswith("Anon"):
- name = name[4:]
- if name.endswith("Handler"):
- name = name[:-7]
- return re.sub('maas', 'MAAS', name, flags=re.IGNORECASE)
+ return helpers.derive_resource_name(self.__handler["name"])
@property
def uri(self):
@@ -199,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):
@@ -222,7 +215,8 @@ def session(self):
@property
def actions(self):
return [
- (name, value) for name, value in vars(self).items()
+ (name, value)
+ for name, value in vars(self).items()
if not name.startswith("_") and isinstance(value, ActionAPI)
]
@@ -291,6 +285,7 @@ def bind(self, **params):
"""
return CallAPI(params, self)
+ @asynchronous
async def __call__(self, **data):
"""Convenience method to do ``this.bind(**params).call(**data).data``.
@@ -298,36 +293,54 @@ async def __call__(self, **data):
Whatever remains is assumed to be data to be passed to ``call()`` as
keyword arguments.
+ All keys in ``params`` that are prefixed with '_' are remapped without
+ the '_' prefix into the ``call()``. This is used when the ``params``
+ and data passed to the call have the same key.
+
:raise KeyError: If not all required arguments are provided.
See `CallAPI.call()` for return information and exceptions.
"""
+ data = dict(data)
params = {name: data.pop(name) for name in self.handler.params}
+ for key, value in data.copy().items():
+ if isinstance(value, typing.Mapping):
+ del data[key]
+ for nested_key, nested_value in value.items():
+ data[key + "_" + nested_key] = nested_value
+ for key, value in data.copy().items():
+ if key.startswith("_"):
+ data[key[1:]] = data.pop(key)
response = await self.bind(**params).call(**data)
return response.data
def __repr__(self):
if self.op is None:
- return "" % (
- self.fullname, self.method, self.handler.uri)
+ return "" % (self.fullname, self.method, self.handler.uri)
else:
return "" % (
- self.fullname, self.method, self.handler.uri, self.op)
+ self.fullname,
+ self.method,
+ self.handler.uri,
+ self.op,
+ )
CallResult = namedtuple("CallResult", ("response", "content", "data"))
class CallError(Exception):
-
def __init__(self, request, response, content, call):
desc_for_request = "%(method)s %(uri)s" % request
desc_for_response = "HTTP %s %s" % (response.status, response.reason)
desc_for_content = content.decode("utf-8", "replace")
desc = "%s -> %s (%s)" % (
- desc_for_request, desc_for_response,
- desc_for_content if len(desc_for_content) <= 50 else (
- desc_for_content[:49] + "…"))
+ desc_for_request,
+ desc_for_response,
+ desc_for_content
+ if len(desc_for_content) <= 50
+ else (desc_for_content[:49] + "…"),
+ )
super(CallError, self).__init__(desc)
self.request = request
self.response = response
@@ -336,11 +349,10 @@ def __init__(self, request, response, content, call):
@property
def status(self):
- return int(self.response["status"])
+ return self.response.status
class CallAPI:
-
def __init__(self, params, action):
"""Create a new `CallAPI`.
@@ -360,9 +372,10 @@ def __validate(self):
raise TypeError("%s takes no arguments" % self.action.fullname)
else:
params_expected_desc = ", ".join(sorted(params_expected))
- raise TypeError("%s takes %d arguments: %s" % (
- self.action.fullname, len(params_expected),
- params_expected_desc))
+ raise TypeError(
+ "%s takes %d arguments: %s"
+ % (self.action.fullname, len(params_expected), params_expected_desc)
+ )
@property
def action(self):
@@ -375,7 +388,10 @@ def uri(self):
# TODO: this is el-cheapo URI Template
# support; use uritemplate-py
# here?
- return self.action.handler.uri.format(**self.__params)
+ uri = urlparse(self.action.handler.uri)
+ if uri.scheme != self.action.handler.session.scheme:
+ uri = uri._replace(scheme=self.action.handler.session.scheme)
+ return uri.geturl().format(**self.__params)
def rebind(self, **params):
"""Rebind the parameters into the URI.
@@ -403,9 +419,10 @@ def prepare(self, data):
:param data: Data to pass in the *body* of the request.
:type data: dict
"""
+
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:
@@ -421,7 +438,8 @@ def expand(data):
# Bundle things up ready to throw over the wire.
uri, body, headers = utils.prepare_payload(
- self.action.op, self.action.method, self.uri, data)
+ self.action.op, self.action.method, self.uri, data
+ )
# Headers are returned as a list, but they must be a dict for
# the signing machinery.
@@ -434,6 +452,7 @@ def expand(data):
return uri, body, headers
+ @asynchronous
async def dispatch(self, uri, body, headers):
"""Dispatch the call via HTTP.
@@ -445,8 +464,8 @@ async def dispatch(self, uri, body, headers):
session = aiohttp.ClientSession(connector=connector)
async with session:
response = await session.request(
- self.action.method, uri, data=body,
- headers=_prefer_json(headers))
+ self.action.method, uri, data=body, headers=_prefer_json(headers)
+ )
async with response:
# Fetch the raw body content.
content = await response.read()
@@ -468,14 +487,14 @@ async def dispatch(self, uri, body, headers):
# Decode from JSON if that's what it's declared as.
if response.content_type is None:
data = await response.read()
- elif response.content_type.endswith('/json'):
+ elif response.content_type.endswith("/json"):
data = await response.json()
else:
data = await response.read()
if response.content_type is None:
data = content
- elif response.content_type.endswith('/json'):
+ elif response.content_type.endswith("/json"):
# JSON should always be UTF-8.
data = json.loads(content.decode("utf-8"))
else:
diff --git a/maas/client/bones/helpers.py b/maas/client/bones/helpers.py
new file mode 100644
index 00000000..a875b995
--- /dev/null
+++ b/maas/client/bones/helpers.py
@@ -0,0 +1,305 @@
+"""Miscellaneous helpers for Bones."""
+
+__all__ = [
+ "authenticate",
+ "connect",
+ "ConnectError",
+ "derive_resource_name",
+ "fetch_api_description",
+ "login",
+ "LoginError",
+ "LoginNotSupported",
+ "PasswordWithoutUsername",
+ "RemoteError",
+ "UsernameWithoutPassword",
+]
+
+import asyncio
+from concurrent import futures
+from getpass import getuser
+from http import HTTPStatus
+from socket import gethostname
+import typing
+from urllib.parse import ParseResult, SplitResult, urljoin, urlparse
+
+import aiohttp
+from macaroonbakery import httpbakery
+
+from ..utils import api_url
+from ..utils.maas_async import asynchronous
+from ..utils.creds import Credentials
+from ..utils.profiles import Profile
+
+
+class RemoteError(Exception):
+ """Miscellaneous error related to a remote system."""
+
+
+async def fetch_api_description(
+ url: typing.Union[str, ParseResult, SplitResult], insecure: bool = False
+):
+ """Fetch the API description from the remote MAAS instance."""
+ url_describe = urljoin(_ensure_url_string(url), "describe/")
+ connector = aiohttp.TCPConnector(verify_ssl=(not insecure))
+ session = aiohttp.ClientSession(connector=connector)
+ async with session, session.get(url_describe) as response:
+ if response.status != HTTPStatus.OK:
+ raise RemoteError("{0} -> {1.status} {1.reason}".format(url, response))
+ elif response.content_type != "application/json":
+ raise RemoteError(
+ "Expected application/json, got: %s" % response.content_type
+ )
+ else:
+ return await response.json()
+
+
+def _ensure_url_string(url):
+ """Convert `url` to a string URL if it isn't one already."""
+ if isinstance(url, str):
+ return url
+ elif isinstance(url, (ParseResult, SplitResult)):
+ return url.geturl()
+ else:
+ raise TypeError("Could not convert %r to a string URL." % (url,))
+
+
+def derive_resource_name(name):
+ """A stable, human-readable name and identifier for a resource."""
+ if name.startswith("Anon"):
+ name = name[4:]
+ if name.endswith("Handler"):
+ name = name[:-7]
+ if name == "Maas":
+ name = "MAAS"
+ return name
+
+
+class ConnectError(Exception):
+ """An error with connecting."""
+
+
+@asynchronous
+async def connect(url, *, apikey=None, insecure=False):
+ """Connect to a remote MAAS instance with `apikey`.
+
+ Returns a new :class:`Profile` which has NOT been saved. To connect AND
+ save a new profile::
+
+ profile = connect(url, apikey=apikey)
+ profile = profile.replace(name="mad-hatter")
+
+ with profiles.ProfileStore.open() as config:
+ config.save(profile)
+ # Optionally, set it as the default.
+ config.default = profile.name
+
+ """
+ url = api_url(url)
+ url = urlparse(url)
+
+ if url.username is not None:
+ raise ConnectError(
+ "Cannot provide user-name explicitly in URL (%r) when connecting; "
+ "use login instead." % url.username
+ )
+ if url.password is not None:
+ raise ConnectError(
+ "Cannot provide password explicitly in URL (%r) when connecting; "
+ "use login instead." % url.username
+ )
+
+ if apikey is None:
+ credentials = None # Anonymous access.
+ else:
+ credentials = Credentials.parse(apikey)
+
+ description = await fetch_api_description(url, insecure)
+
+ # Return a new (unsaved) profile.
+ return Profile(
+ name=url.netloc,
+ url=url.geturl(),
+ credentials=credentials,
+ description=description,
+ insecure=insecure,
+ )
+
+
+class LoginError(Exception):
+ """An error with logging-in."""
+
+
+class PasswordWithoutUsername(LoginError):
+ """A password was provided without a corresponding user-name."""
+
+
+class UsernameWithoutPassword(LoginError):
+ """A user-name was provided without a corresponding password."""
+
+
+class LoginNotSupported(LoginError):
+ """Server does not support login-type auth for API clients."""
+
+
+class MacaroonLoginNotSupported(LoginError):
+ """Server does not support macaroon auth for API clients."""
+
+
+@asynchronous
+async def login(url, *, anonymous=False, username=None, password=None, insecure=False):
+ """Log-in to a remote MAAS instance.
+
+ Returns a new :class:`Profile` which has NOT been saved. To log-in AND
+ save a new profile::
+
+ profile = login(url, username="alice", password="wonderland")
+ profile = profile.replace(name="mad-hatter")
+
+ with profiles.ProfileStore.open() as config:
+ config.save(profile)
+ # Optionally, set it as the default.
+ config.default = profile.name
+
+ :raise RemoteError: An unexpected error from the remote system.
+ :raise LoginError: An error related to logging-in.
+ :raise PasswordWithoutUsername: Password given, but not username.
+ :raise UsernameWithoutPassword: Username given, but not password.
+ :raise LoginNotSupported: Server does not support API client log-in.
+ """
+ url = api_url(url)
+ url = urlparse(url)
+
+ if username is None:
+ username = url.username
+ else:
+ if url.username is None:
+ pass # Anonymous access.
+ else:
+ raise LoginError(
+ "User-name provided explicitly (%r) and in URL (%r); "
+ "provide only one." % (username, url.username)
+ )
+
+ if password is None:
+ password = url.password
+ else:
+ if url.password is None:
+ pass # Anonymous access.
+ else:
+ raise LoginError(
+ "Password provided explicitly (%r) and in URL (%r); "
+ "provide only one." % (password, url.password)
+ )
+
+ # Remove user-name and password from the URL.
+ userinfo, _, hostinfo = url.netloc.rpartition("@")
+ url = url._replace(netloc=hostinfo)
+
+ if username is None:
+ if password:
+ raise PasswordWithoutUsername(
+ "Password provided without user-name; specify user-name."
+ )
+ elif anonymous:
+ credentials = None
+ else:
+ credentials = await authenticate_with_macaroon(
+ url.geturl(), insecure=insecure
+ )
+ else:
+ if password is None:
+ raise UsernameWithoutPassword(
+ "User-name provided without password; specify password."
+ )
+ else:
+ credentials = await authenticate(
+ url.geturl(), username, password, insecure=insecure
+ )
+
+ description = await fetch_api_description(url, insecure)
+ profile_name = username or url.netloc
+
+ # Return a new (unsaved) profile.
+ return Profile(
+ name=profile_name,
+ url=url.geturl(),
+ credentials=credentials,
+ description=description,
+ insecure=insecure,
+ )
+
+
+async def authenticate_with_macaroon(url, insecure=False):
+ """Login via macaroons and generate and return new API keys."""
+ executor = futures.ThreadPoolExecutor(max_workers=1)
+
+ def get_token():
+ client = httpbakery.Client()
+ resp = client.request(
+ "POST",
+ "{}/account/?op=create_authorisation_token".format(url),
+ verify=not insecure,
+ )
+ if resp.status_code == HTTPStatus.UNAUTHORIZED:
+ # if the auteentication with Candid fails, an exception is raised
+ # above so we don't get here
+ raise MacaroonLoginNotSupported("Macaroon authentication not supported")
+ if resp.status_code != HTTPStatus.OK:
+ raise LoginError("Login failed: {}".format(resp.text))
+ result = resp.json()
+ return "{consumer_key}:{token_key}:{token_secret}".format(**result)
+
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(executor, get_token)
+
+
+async def authenticate(url, username, password, *, insecure=False):
+ """Obtain a new API key by logging into MAAS.
+
+ :param url: URL for the MAAS API (i.e. ends with ``/api/x.y/``).
+ :param insecure: If true, don't verify SSL/TLS certificates.
+ :return: A `Credentials` instance.
+
+ :raise RemoteError: An unexpected error from the remote system.
+ :raise LoginNotSupported: Server does not support API client log-in.
+ """
+ url_versn = urljoin(url, "version/")
+ url_authn = urljoin(url, "../../accounts/authenticate/")
+
+ def check_response_is_okay(response):
+ if response.status != HTTPStatus.OK:
+ raise RemoteError(
+ "{0} -> {1.status} {1.reason}".format(
+ response.url_obj.human_repr(), response
+ )
+ )
+
+ connector = aiohttp.TCPConnector(verify_ssl=(not insecure))
+ session = aiohttp.ClientSession(connector=connector)
+ async with session:
+ # Check that this server supports `authenticate-api`.
+ async with session.get(url_versn) as response:
+ check_response_is_okay(response)
+ version_info = await response.json()
+
+ if "authenticate-api" not in version_info["capabilities"]:
+ raise LoginNotSupported(
+ "Server does not support automated client log-in. "
+ "Please obtain an API token via the MAAS UI."
+ )
+
+ # POST to the `authenticate` endpoint.
+ data = {
+ "username": username,
+ "password": password,
+ "consumer": "%s@%s" % (getuser(), gethostname()),
+ }
+ async with session.post(url_authn, data=data) as response:
+ check_response_is_okay(response)
+ token_info = await response.json()
+
+ return Credentials(
+ token_info["consumer_key"],
+ token_info["token_key"],
+ token_info["token_secret"],
+ )
diff --git a/maas/client/bones/testing/__init__.py b/maas/client/bones/testing/__init__.py
new file mode 100644
index 00000000..9577e755
--- /dev/null
+++ b/maas/client/bones/testing/__init__.py
@@ -0,0 +1,100 @@
+"""Testing helpers for the Bones API."""
+
+__all__ = ["api_descriptions", "DescriptionServer", "list_api_descriptions"]
+
+import http
+import http.server
+import json
+from operator import itemgetter
+from pathlib import Path
+import re
+import threading
+
+import fixtures
+from pkg_resources import resource_filename, resource_listdir
+
+
+def list_api_descriptions():
+ """List API description documents.
+
+ They're searched for in the same directory as this file, and their name
+ must match "apiXX.json" where "XX" denotes the major and minor version
+ number of the API.
+ """
+ for filename in resource_listdir(__name__, "."):
+ match = re.match(r"api(\d)(\d)[.]json", filename)
+ if match is not None:
+ version = tuple(map(int, match.groups()))
+ path = resource_filename(__name__, filename)
+ name = "%d.%d" % version
+ yield name, version, Path(path)
+
+
+def load_api_descriptions():
+ """Load the API description documents found by `list_api_descriptions`."""
+ for name, version, path in list_api_descriptions():
+ description = path.read_text("utf-8")
+ yield name, version, json.loads(description)
+
+
+api_descriptions = sorted(load_api_descriptions(), key=itemgetter(1))
+assert len(api_descriptions) != 0
+
+
+class DescriptionHandler(http.server.BaseHTTPRequestHandler):
+ """An HTTP request handler that serves only API descriptions.
+
+ The `desc` attribute ought to be specified, for example by subclassing, or
+ by using the `make` class-method.
+
+ The `content_type` attribute can be overridden to simulate a different
+ Content-Type header for the description.
+ """
+
+ # Override these in subclasses.
+ description = b'{"resources": []}'
+ content_type = "application/json"
+
+ @classmethod
+ def make(cls, description=description):
+ return type("DescriptionHandler", (cls,), {"description": description})
+
+ def setup(self):
+ super(DescriptionHandler, self).setup()
+ self.logs = []
+
+ def log_message(self, *args):
+ """By default logs go to stdout/stderr. Instead, capture them."""
+ self.logs.append(args)
+
+ def do_GET(self):
+ version_match = re.match(r"/MAAS/api/([0-9.]+)/describe/$", self.path)
+ if version_match is None:
+ self.send_error(http.HTTPStatus.NOT_FOUND)
+ else:
+ self.send_response(http.HTTPStatus.OK)
+ self.send_header("Content-Type", self.content_type)
+ self.send_header("Content-Length", str(len(self.description)))
+ self.end_headers()
+ self.wfile.write(self.description)
+
+
+class DescriptionServer(fixtures.Fixture):
+ """Fixture to start up an HTTP server for API descriptions only.
+
+ :ivar handler: A `DescriptionHandler` subclass.
+ :ivar server: An `http.server.HTTPServer` instance.
+ :ivar url: A URL that points to the API that `server` is mocking.
+ """
+
+ def __init__(self, description=DescriptionHandler.description):
+ super(DescriptionServer, self).__init__()
+ self.description = description
+
+ def _setUp(self):
+ self.handler = DescriptionHandler.make(self.description)
+ self.server = http.server.HTTPServer(("", 0), self.handler)
+ self.url = "http://%s:%d/MAAS/api/2.0/" % self.server.server_address
+ threading.Thread(target=self.server.serve_forever).start()
+ self.addCleanup(self.server.server_close)
+ self.addCleanup(self.server.shutdown)
diff --git a/maas/client/bones/testing/api20.json b/maas/client/bones/testing/api20.json
new file mode 100644
index 00000000..6f2c7bba
--- /dev/null
+++ b/maas/client/bones/testing/api20.json
@@ -0,0 +1,2731 @@
+{
+ "doc": "MAAS API",
+ "hash": "ad0d8bb110f4b629278ceaef279948b7cf33db41",
+ "resources": [
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:return: a json dict with three keys: 'token_key',\n 'token_secret' and 'consumer_key' (e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg'}).\n:rtype: string (json)",
+ "method": "POST",
+ "name": "create_authorisation_token",
+ "op": "create_authorisation_token",
+ "restful": false
+ },
+ {
+ "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode",
+ "method": "POST",
+ "name": "delete_authorisation_token",
+ "op": "delete_authorisation_token",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the current logged-in user.",
+ "name": "AccountHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/"
+ },
+ "name": "AccountHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete cache set on node.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the node or cache set is not found.\nReturns 409 if the node is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache cache set on node.\n\nReturns 404 if the node or cache set is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on node.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the node or the cache set is not found.\nReturns 409 if the node is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache set on a node.",
+ "name": "BcacheCacheSetHandler",
+ "params": [
+ "system_id",
+ "cache_set_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{cache_set_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{cache_set_id}/"
+ },
+ "name": "BcacheCacheSetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache cache sets belonging to node.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache sets on a node.",
+ "name": "BcacheCacheSetsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/"
+ },
+ "name": "BcacheCacheSetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete bcache on node.\n\nReturns 404 if the node or bcache is not found.\nReturns 409 if the node is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache device on node.\n\nReturns 404 if the node or bcache is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on node.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the node or the bcache is not found.\nReturns 409 if the node is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache device on a node.",
+ "name": "BcacheHandler",
+ "params": [
+ "system_id",
+ "bcache_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{bcache_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{bcache_id}/"
+ },
+ "name": "BcacheHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache devices belonging to node.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache devices on a node.",
+ "name": "BcachesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/"
+ },
+ "name": "BcachesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a tag to block device on node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the node is not Ready.",
+ "method": "GET",
+ "name": "add_tag",
+ "op": "add_tag",
+ "restful": false
+ },
+ {
+ "doc": "Delete block device on node.\n\nReturns 404 if the node or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the node is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the node or block device is not found.\nReturns 409 if the node is not Ready or Allocated.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the node or block device is not found.\nReturns 409 if the node is not Ready or Allocated.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read block device on node.\n\nReturns 404 if the node or block device is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Remove a tag from block device on node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the node is not Ready.",
+ "method": "GET",
+ "name": "remove_tag",
+ "op": "remove_tag",
+ "restful": false
+ },
+ {
+ "doc": "Set this block device as the boot disk for the node.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the node or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the node is not Ready or Allocated.",
+ "method": "POST",
+ "name": "set_boot_disk",
+ "op": "set_boot_disk",
+ "restful": false
+ },
+ {
+ "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the node or block device is not found.\nReturns 409 if the node is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the node or block device is not found.\nReturns 409 if the node is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ },
+ {
+ "doc": "Update block device on node.\n\nFields for physical block device:\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the node or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the node is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a block device on a node.",
+ "name": "BlockDeviceHandler",
+ "params": [
+ "system_id",
+ "device_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/"
+ },
+ "name": "BlockDeviceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all block devices belonging to node.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage block devices on a node.",
+ "name": "BlockDevicesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/"
+ },
+ "name": "BlockDevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete boot resource.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot resource.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot resource.",
+ "name": "BootResourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-resources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/"
+ },
+ "name": "BootResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Import the boot resources.",
+ "method": "POST",
+ "name": "import",
+ "op": "import",
+ "restful": false
+ },
+ {
+ "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the boot resources.",
+ "name": "BootResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-resources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/"
+ },
+ "name": "BootResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_filename: The GPG keyring for this BootSource,\n base64-encoded data.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source.",
+ "name": "BootSourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/"
+ },
+ "name": "BootSourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source selection.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source selection.",
+ "name": "BootSourceSelectionHandler",
+ "params": [
+ "boot_source_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/"
+ },
+ "name": "BootSourceSelectionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot source selections.\n\nGet a listing of a boot source's selections.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot source selections.",
+ "name": "BootSourceSelectionsHandler",
+ "params": [
+ "boot_source_id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/"
+ },
+ "name": "BootSourceSelectionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot sources.\n\nGet a listing of boot sources.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot sources.",
+ "name": "BootSourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-sources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/"
+ },
+ "name": "BootSourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a commissioning script.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a commissioning script.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a commissioning script.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/commissioning-scripts/{name}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}"
+ },
+ "name": "CommissioningScriptHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning node will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning node in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List commissioning scripts.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/commissioning-scripts/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/"
+ },
+ "name": "CommissioningScriptsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresource.",
+ "name": "DNSResourceHandler",
+ "params": [
+ "dnsresource_id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresources/{dnsresource_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{dnsresource_id}/"
+ },
+ "name": "DNSResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecord.",
+ "name": "DNSResourceRecordHandler",
+ "params": [
+ "dnsresourcerecord_id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/{dnsresourcerecord_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{dnsresourcerecord_id}/"
+ },
+ "name": "DNSResourceRecordHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecords.",
+ "name": "DNSResourceRecordsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/"
+ },
+ "name": "DNSResourceRecordsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresources.",
+ "name": "DNSResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/"
+ },
+ "name": "DNSResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a specific device.\n\nReturns 404 if the device is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type hostname: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual device.\n\nThe device is identified by its system_id.",
+ "name": "DeviceHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/devices/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/"
+ },
+ "name": "DeviceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new device.\n\n:param mac_addresses: One or more MAC addresses for the device.\n:param hostname: A hostname. If not given, one will be generated.\n:param parent: The system id of the parent. Optional.",
+ "method": "POST",
+ "name": "create",
+ "op": "create",
+ "restful": false
+ },
+ {
+ "doc": "Create a new device.\n\n:param mac_addresses: One or more MAC addresses for the device.\n:param hostname: A hostname. If not given, one will be generated.\n:param parent: The system id of the parent. Optional.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List devices visible to the user, optionally filtered by criteria.\n\n:param hostname: An optional list of hostnames. Only devices with\n matching hostnames will be returned.\n:type hostname: iterable\n:param mac_address: An optional list of MAC addresses. Only\n devices with matching MAC addresses will be returned.\n:type mac_address: iterable\n:param id: An optional list of system ids. Only devices with\n matching system ids will be returned.\n:type id: iterable",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the devices in the MAAS.",
+ "name": "DevicesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/devices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/"
+ },
+ "name": "DevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read domain.\n\nReturns 404 if the domain is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage domain.",
+ "name": "DomainHandler",
+ "params": [
+ "domain_id"
+ ],
+ "path": "/MAAS/api/2.0/domains/{domain_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/{domain_id}/"
+ },
+ "name": "DomainHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all domains.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage domains.",
+ "name": "DomainsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/domains/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/"
+ },
+ "name": "DomainsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: WARNING, ERROR, INFO, CRITICAL, DEBUG.\n The default is INFO.",
+ "method": "GET",
+ "name": "query",
+ "op": "query",
+ "restful": false
+ }
+ ],
+ "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.",
+ "name": "EventsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/events/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/events/"
+ },
+ "name": "EventsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabric.",
+ "name": "FabricHandler",
+ "params": [
+ "fabric_id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/"
+ },
+ "name": "FabricHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param class_type: Class type of the fabric.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fabrics.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabrics.",
+ "name": "FabricsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fabrics/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/"
+ },
+ "name": "FabricsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Network.",
+ "name": "FanNetworkHandler",
+ "params": [
+ "fannetwork_id"
+ ],
+ "path": "/MAAS/api/2.0/fannetworks/{fannetwork_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{fannetwork_id}/"
+ },
+ "name": "FanNetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fannetworks.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Networks.",
+ "name": "FanNetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fannetworks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/"
+ },
+ "name": "FanNetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a FileStorage object.",
+ "method": "POST",
+ "name": "delete",
+ "op": "delete",
+ "restful": false
+ },
+ {
+ "doc": "Delete a FileStorage object.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.",
+ "name": "FileHandler",
+ "params": [
+ "filename"
+ ],
+ "path": "/MAAS/api/2.0/files/{filename}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/"
+ },
+ "name": "FileHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get",
+ "op": "get",
+ "restful": false
+ },
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.",
+ "name": "AnonFilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied",
+ "method": "POST",
+ "name": "add",
+ "op": "add",
+ "restful": false
+ },
+ {
+ "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get",
+ "op": "get",
+ "restful": false
+ },
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ },
+ {
+ "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string",
+ "method": "GET",
+ "name": "list",
+ "op": "list",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the files in this MAAS.",
+ "name": "FilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "name": "FilesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List IPAddresses.\n\nGet a listing of all IPAddresses allocated to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\nReturns 404 if the provided IP address is not found.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip_address: The IP address, which must be within\n a known subnet.\n:param hostname: The hostname to use for the specified IP address\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available.",
+ "method": "POST",
+ "name": "reserve",
+ "op": "reserve",
+ "restful": false
+ }
+ ],
+ "doc": "Manage IP addresses allocated by MAAS.",
+ "name": "IPAddressesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/ipaddresses/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/"
+ },
+ "name": "IPAddressesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "link_subnet",
+ "op": "link_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "set_default_gateway",
+ "op": "set_default_gateway",
+ "restful": false
+ },
+ {
+ "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "unlink_subnet",
+ "op": "unlink_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Update interface on node.\n\nFields for physical interface:\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to.\n\nFields for bond interface:\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond-mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond-miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond-downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond-updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond-lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond-xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nReturns 404 if the node or interface is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a node's or device's interface.",
+ "name": "InterfaceHandler",
+ "params": [
+ "system_id",
+ "interface_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{interface_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{interface_id}/"
+ },
+ "name": "InterfaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bond",
+ "op": "create_bond",
+ "restful": false
+ },
+ {
+ "doc": "Create a physical interface on a machine, device, or\nrack controller.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_physical",
+ "op": "create_physical",
+ "restful": false
+ },
+ {
+ "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_vlan",
+ "op": "create_vlan",
+ "restful": false
+ },
+ {
+ "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage interfaces on a node or device.",
+ "name": "InterfacesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/"
+ },
+ "name": "InterfacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete license key.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read license key.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a license key.",
+ "name": "LicenseKeyHandler",
+ "params": [
+ "osystem",
+ "distro_series"
+ ],
+ "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}"
+ },
+ "name": "LicenseKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List license keys.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the license keys.",
+ "name": "LicenseKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/license-keys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/"
+ },
+ "name": "LicenseKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n:type name: unicode\n\nAvailable configuration items:\n- default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n- main_archive: Main archive. Archive used by nodes to retrieve packages for Intel architectures. E.g. http://archive.ubuntu.com/ubuntu.\n- curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n- kernel_opts: Boot parameters to pass to the kernel by default.\n- upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n- maas_name: MAAS name.\n- enable_disk_erasing_on_release: Erase nodes' disks prior to releasing..\n- http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n- enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n- default_distro_series: Default OS release used for deployment.\n- windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)\n- dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n- boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n- ntp_server: Address of NTP server for nodes. NTP server address passed to nodes via a DHCP response. e.g. ntp.ubuntu.com\n- commissioning_distro_series: Default Ubuntu release used for commissioning.\n- default_osystem: Default operating system used for deployment.\n- default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'flat' (Flat layout), 'lvm' (LVM layout), 'bcache' (Bcache layout).\n- default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n- ports_archive: Ports archive. Archive used by nodes to retrieve packages for non-Intel architectures. E.g. http://ports.ubuntu.com/ubuntu-ports.\n- enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).",
+ "method": "GET",
+ "name": "get_config",
+ "op": "get_config",
+ "restful": false
+ },
+ {
+ "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:type name: unicode\n:param value: The value of the config item to be set.\n:type value: json object\n\nAvailable configuration items:\n- default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n- main_archive: Main archive. Archive used by nodes to retrieve packages for Intel architectures. E.g. http://archive.ubuntu.com/ubuntu.\n- curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n- kernel_opts: Boot parameters to pass to the kernel by default.\n- upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n- maas_name: MAAS name.\n- enable_disk_erasing_on_release: Erase nodes' disks prior to releasing..\n- http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n- enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n- default_distro_series: Default OS release used for deployment.\n- windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)\n- dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n- boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n- ntp_server: Address of NTP server for nodes. NTP server address passed to nodes via a DHCP response. e.g. ntp.ubuntu.com\n- commissioning_distro_series: Default Ubuntu release used for commissioning.\n- default_osystem: Default operating system used for deployment.\n- default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'flat' (Flat layout), 'lvm' (LVM layout), 'bcache' (Bcache layout).\n- default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n- ports_archive: Ports archive. Archive used by nodes to retrieve packages for non-Intel architectures. E.g. http://ports.ubuntu.com/ubuntu-ports.\n- enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).",
+ "method": "POST",
+ "name": "set_config",
+ "op": "set_config",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the MAAS server.",
+ "name": "MaasHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/maas/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/maas/"
+ },
+ "name": "MaasHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Abort a machine's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nThis currently only supports aborting of the 'Disk Erasing' operation.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.",
+ "method": "POST",
+ "name": "abort",
+ "op": "abort",
+ "restful": false
+ },
+ {
+ "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `node-interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways.",
+ "method": "POST",
+ "name": "clear_default_gateways",
+ "op": "clear_default_gateways",
+ "restful": false
+ },
+ {
+ "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found.",
+ "method": "POST",
+ "name": "commission",
+ "op": "commission",
+ "restful": false
+ },
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "deploy",
+ "op": "deploy",
+ "restful": false
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration.",
+ "method": "GET",
+ "name": "get_curtin_config",
+ "op": "get_curtin_config",
+ "restful": false
+ },
+ {
+ "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the Node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.",
+ "method": "POST",
+ "name": "mark_broken",
+ "op": "mark_broken",
+ "restful": false
+ },
+ {
+ "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nfixed.",
+ "method": "POST",
+ "name": "mark_fixed",
+ "op": "mark_fixed",
+ "restful": false
+ },
+ {
+ "doc": "Power off a machine.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the machine's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to stop the machine.",
+ "method": "POST",
+ "name": "power_off",
+ "op": "power_off",
+ "restful": false
+ },
+ {
+ "doc": "Turn on a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "power_on",
+ "op": "power_on",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Query the power state of a node.\n\nSend a request to the machine's power controller which asks it about\nthe machine's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The machine to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the machine is not found.\nReturns 503 (with explanatory text) if the power state could not\nbe queried.",
+ "method": "GET",
+ "name": "query_power_state",
+ "op": "query_power_state",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release a node. Opposite of `MachinesHandler.acquire`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout.",
+ "method": "POST",
+ "name": "set_storage_layout",
+ "op": "set_storage_layout",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. For instance, if the power_type is 'ether_wake', the\n only valid parameter is 'power_address' so one would want to pass\n 'myaddress' as the value of the 'power_parameters_power_address'\n parameter. Available to admin users. See the `Power types`_ section\n for a list of the available power parameters for each power type.\n:type power_parameters_{param1}: unicode\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n:param zone: Name of a valid physical zone in which to place this\n machine\n:type zone: unicode\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.",
+ "name": "MachineHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/machines/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/"
+ },
+ "name": "MachineHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\nautodetect_nodegroup=True\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:param hostname: A hostname. If not given, one will be generated.\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:param autodetect_nodegroup: (boolean) Whether or not to attempt\n nodegroup detection for this machine. The nodegroup is determined\n based on the requestor's IP address range. (if the API request\n comes from an IP range within a known nodegroup, that nodegroup\n will be used.)\n:param nodegroup: The id of the nodegroup this machine belongs to.",
+ "method": "POST",
+ "name": "create",
+ "op": "create",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\nautodetect_nodegroup=True\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:param hostname: A hostname. If not given, one will be generated.\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:param autodetect_nodegroup: (boolean) Whether or not to attempt\n nodegroup detection for this machine. The nodegroup is determined\n based on the requestor's IP address range. (if the API request\n comes from an IP range within a known nodegroup, that nodegroup\n will be used.)\n:param nodegroup: The id of the nodegroup this machine belongs to.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Anonymous access to Machines.",
+ "name": "AnonMachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.",
+ "method": "POST",
+ "name": "accept_all",
+ "op": "accept_all",
+ "restful": false
+ },
+ {
+ "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname of the returned machine.\n:type name: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n:type arch: unicode\n:param cpu_count: The minium number of CPUs the returned machine must\n have.\n:type cpu_count: int\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have.\n:type mem: float\n:param tags: List of tags the returned machine must have.\n:type tags: list of unicodes\n:param not_tags: List of tags the acquired machine must not have.\n:type tags: List of unicodes.\n:param networks: List of networks (defined in MAAS) to which the\n machine must be attached. A network can be identified by the name\n assigned to it in MAAS; or by an `ip:` prefix followed by any IP\n address that falls within the network; or a `vlan:` prefix\n followed by a numeric VLAN tag, e.g. `vlan:23` for VLAN number 23.\n Valid VLAN tags must be in the range of 1 to 4095 inclusive.\n:type networks: list of unicodes\n:param not_networks: List of networks (defined in MAAS) to which the\n machine must not be attached. The returned machine won't be\n attached to any of the specified networks. A network can be\n identified by the name assigned to it in MAAS; or by an `ip:`\n prefix followed by any IP address that falls within the network; or\n a `vlan:` prefix followed by a numeric VLAN tag, e.g. `vlan:23` for\n VLAN number 23. Valid VLAN tags must be in the range of 1 to 4095\n inclusive.\n:type not_networks: list of unicodes\n:param zone: An optional name for a physical zone the acquired\n machine should be located in.\n:type zone: unicode\n:type not_in_zone: Optional list of physical zones from which the\n machine should not be acquired.\n:type not_in_zone: List of unicodes.\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound.",
+ "method": "POST",
+ "name": "allocate",
+ "op": "allocate",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\nautodetect_nodegroup=True\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:param hostname: A hostname. If not given, one will be generated.\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:param autodetect_nodegroup: (boolean) Whether or not to attempt\n nodegroup detection for this machine. The nodegroup is determined\n based on the requestor's IP address range. (if the API request\n comes from an IP range within a known nodegroup, that nodegroup\n will be used.)\n:param nodegroup: The id of the nodegroup this node belongs to.",
+ "method": "POST",
+ "name": "create",
+ "op": "create",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\nautodetect_nodegroup=True\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:param hostname: A hostname. If not given, one will be generated.\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:param autodetect_nodegroup: (boolean) Whether or not to attempt\n nodegroup detection for this machine. The nodegroup is determined\n based on the requestor's IP address range. (if the API request\n comes from an IP range within a known nodegroup, that nodegroup\n will be used.)\n:param nodegroup: The id of the nodegroup this node belongs to.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Retrieve deployment status for multiple machines.\n\n:param machines: Mandatory list of system IDs for machines whose status\n you wish to check.\n\nReturns 400 if mandatory parameters are missing.\nReturns 403 if the user has no permission to view any of the machines.",
+ "method": "GET",
+ "name": "deployment_status",
+ "op": "deployment_status",
+ "restful": false
+ },
+ {
+ "doc": "Fetch Machines that were allocated to the User/oauth token.",
+ "method": "GET",
+ "name": "list_allocated",
+ "op": "list_allocated",
+ "restful": false
+ },
+ {
+ "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "List all nodes.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the nodes in the MAAS.",
+ "name": "MachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "name": "MachinesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "connect_macs",
+ "op": "connect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "disconnect_macs",
+ "op": "disconnect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned.",
+ "method": "GET",
+ "name": "list_connected_macs",
+ "op": "list_connected_macs",
+ "restful": false
+ },
+ {
+ "doc": "Read network definition.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.",
+ "name": "NetworkHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/networks/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/"
+ },
+ "name": "NetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.",
+ "name": "NetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/networks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/"
+ },
+ "name": "NetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the Node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.",
+ "method": "POST",
+ "name": "mark_broken",
+ "op": "mark_broken",
+ "restful": false
+ },
+ {
+ "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nfixed.",
+ "method": "POST",
+ "name": "mark_fixed",
+ "op": "mark_fixed",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": null,
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.",
+ "name": "NodeHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/"
+ },
+ "name": "NodeHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "link_subnet",
+ "op": "link_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "set_default_gateway",
+ "op": "set_default_gateway",
+ "restful": false
+ },
+ {
+ "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "unlink_subnet",
+ "op": "unlink_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Update interface on node.\n\nFields for physical interface:\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to.\n\nFields for bond interface:\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond-mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond-miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond-downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond-updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond-lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond-xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nReturns 404 if the node or interface is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a node's interface. (Deprecated)",
+ "name": "NodeInterfaceHandler",
+ "params": [
+ "system_id",
+ "interface_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{interface_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{interface_id}/"
+ },
+ "name": "NodeInterfaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bond",
+ "op": "create_bond",
+ "restful": false
+ },
+ {
+ "doc": "Create a physical interface on a machine, device, or\nrack controller.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_physical",
+ "op": "create_physical",
+ "restful": false
+ },
+ {
+ "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_vlan",
+ "op": "create_vlan",
+ "restful": false
+ },
+ {
+ "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage interfaces on a node. (Deprecated)",
+ "name": "NodeInterfacesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/"
+ },
+ "name": "NodeInterfacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable",
+ "method": "GET",
+ "name": "list",
+ "op": "list",
+ "restful": false
+ }
+ ],
+ "doc": "Read the collection of NodeResult in the MAAS.",
+ "name": "NodeResultsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/installation-results/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/"
+ },
+ "name": "NodeResultsHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List all nodes.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the nodes in the MAAS.",
+ "name": "NodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "name": "NodesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Unformat a partition.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ }
+ ],
+ "doc": "Manage partition on a block device.",
+ "name": "PartitionHandler",
+ "params": [
+ "system_id",
+ "device_id",
+ "partition_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{partition_id}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{partition_id}"
+ },
+ "name": "PartitionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage partitions on a block device.",
+ "name": "PartitionsHandler",
+ "params": [
+ "system_id",
+ "device_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/"
+ },
+ "name": "PartitionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the Node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.",
+ "method": "POST",
+ "name": "mark_broken",
+ "op": "mark_broken",
+ "restful": false
+ },
+ {
+ "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nfixed.",
+ "method": "POST",
+ "name": "mark_fixed",
+ "op": "mark_fixed",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Refresh the hardware information for a specific rack controller.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to refresh the rack.",
+ "method": "POST",
+ "name": "refresh",
+ "op": "refresh",
+ "restful": false
+ },
+ {
+ "doc": null,
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.",
+ "name": "RackControllerHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/"
+ },
+ "name": "RackControllerHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List all nodes.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all rack controllers in MAAS.",
+ "name": "RackControllersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/rackcontrollers/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/"
+ },
+ "name": "RackControllersHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete RAID on node.\n\nReturns 404 if the node or RAID is not found.\nReturns 409 if the node is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read RAID device on node.\n\nReturns 404 if the node or RAID is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update RAID on node.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the node or RAID is not found.\nReturns 409 if the node is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a specific RAID device on a node.",
+ "name": "RaidHandler",
+ "params": [
+ "system_id",
+ "raid_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{raid_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{raid_id}/"
+ },
+ "name": "RaidHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all RAID devices belonging to node.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage all RAID devices on a node.",
+ "name": "RaidsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raids/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/"
+ },
+ "name": "RaidsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.",
+ "method": "POST",
+ "name": "delete",
+ "op": "delete",
+ "restful": false
+ },
+ {
+ "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.",
+ "name": "SSHKeyHandler",
+ "params": [
+ "keyid"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/{keyid}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{keyid}/"
+ },
+ "name": "SSHKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "list",
+ "op": "list",
+ "restful": false
+ },
+ {
+ "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "new",
+ "op": "new",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the SSH keys in this MAAS.",
+ "name": "SSHKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/"
+ },
+ "name": "SSHKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.",
+ "method": "GET",
+ "name": "delete",
+ "op": "delete",
+ "restful": false
+ },
+ {
+ "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSL key.\n\nReturns 404 if the keyid is not found.\nReturns 401 if the key does not belong to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.",
+ "name": "SSLKeyHandler",
+ "params": [
+ "keyid"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/{keyid}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{keyid}/"
+ },
+ "name": "SSLKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "list",
+ "op": "list",
+ "restful": false
+ },
+ {
+ "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "new",
+ "op": "new",
+ "restful": false
+ }
+ ],
+ "doc": "Operations on multiple keys.",
+ "name": "SSLKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/"
+ },
+ "name": "SSLKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete space.\n\nReturns 404 if the space is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read space.\n\nReturns 404 if the space is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update space.\n\n:param name: Name of the space.\n\nReturns 404 if the space is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage space.",
+ "name": "SpaceHandler",
+ "params": [
+ "space_id"
+ ],
+ "path": "/MAAS/api/2.0/spaces/{space_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{space_id}/"
+ },
+ "name": "SpaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a space.\n\n:param name: Name of the space.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all spaces.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage spaces.",
+ "name": "SpacesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/spaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/"
+ },
+ "name": "SpacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional arguments:\nwith_username: (default=True) if False, suppresses the display\nof usernames associated with each address.\nwith_node_summary: (default=True) if False, suppresses the display\nof any node associated with each address.",
+ "method": "GET",
+ "name": "ip_addresses",
+ "op": "ip_addresses",
+ "restful": false
+ },
+ {
+ "doc": "Read subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "reserved_ip_ranges",
+ "op": "reserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Returns statistics for the specified subnet, including:\n\nnum_available - the number of available IP addresses\nlargest_available - the largest number of contiguous free IP addresses\nnum_unavailable - the number of unavailable IP addresses\ntotal_addresses - the sum of the available plus unavailable addresses\nusage - the (floating point) usage percentage of this subnet\nusage_string - the (formatted unicode) usage percentage of this subnet\nranges - the specific IP ranges present in ths subnet (if specified)\n\nOptional arguments:\ninclude_ranges: if True, includes detailed information\nabout the usage of this range.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "statistics",
+ "op": "statistics",
+ "restful": false
+ },
+ {
+ "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "unreserved_ip_ranges",
+ "op": "unreserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Update subnet.\n\n:param name: Name of the subnet.\n:param vlan: VLAN this subnet belongs to.\n:param space: Space this subnet is in.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnet.",
+ "name": "SubnetHandler",
+ "params": [
+ "subnet_id"
+ ],
+ "path": "/MAAS/api/2.0/subnets/{subnet_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{subnet_id}/"
+ },
+ "name": "SubnetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a subnet.\n\n:param name: Name of the subnet.\n:param fabric: Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to or defaults to the default fabric.\n:param vlan: VLAN this subnet belongs to. Defaults to the default\n VLAN for the provided fabric or defaults to the default VLAN in\n the default fabric.\n:param vid: VID of the VLAN this subnet belongs to. Only used when\n vlan is not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n:param space: Space this subnet is in. Defaults to the default space.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means\n no reverse zone is created; Enabled means generate the reverse\n zone; RFC2317 extends Enabled to create the necessary parent zone\n with the appropriate CNAME resource records for the network, if the\n network is small enough to require the support described in\n RFC2317.\n:param dns_servers: Comma-seperated list of DNS servers for this\n subnet.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all subnets.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnets.",
+ "name": "SubnetsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/subnets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/"
+ },
+ "name": "SubnetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "nodes",
+ "op": "nodes",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found.",
+ "method": "POST",
+ "name": "rebuild",
+ "op": "rebuild",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition.",
+ "method": "POST",
+ "name": "update_nodes",
+ "op": "update_nodes",
+ "restful": false
+ }
+ ],
+ "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.",
+ "name": "TagHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/tags/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/"
+ },
+ "name": "TagHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Tags.\n\nGet a listing of all tags that are currently defined.",
+ "method": "GET",
+ "name": "list",
+ "op": "list",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin.",
+ "method": "POST",
+ "name": "new",
+ "op": "new",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the Tags in this MAAS.",
+ "name": "TagsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/tags/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/"
+ },
+ "name": "TagsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List users.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the user accounts of this MAAS.",
+ "name": "UsersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/users/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/users/"
+ },
+ "name": "UsersHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Version and capabilities of this MAAS instance.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }",
+ "name": "VersionHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/version/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/version/"
+ },
+ "auth": null,
+ "name": "VersionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:param vid: VLAN ID of the VLAN.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLAN on a fabric.",
+ "name": "VlanHandler",
+ "params": [
+ "fabric_id",
+ "vid"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/"
+ },
+ "name": "VlanHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param vid: VLAN ID of the VLAN.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLANs on a fabric.",
+ "name": "VlansHandler",
+ "params": [
+ "fabric_id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/"
+ },
+ "name": "VlansHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the node or volume group is not found.\nReturns 409 if the node is not Ready.",
+ "method": "POST",
+ "name": "create_logical_volume",
+ "op": "create_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Delete volume group on node.\n\nReturns 404 if the node or volume group is not found.\nReturns 409 if the node is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the node or volume group is not found.\nReturns 409 if the node is not Ready.",
+ "method": "POST",
+ "name": "delete_logical_volume",
+ "op": "delete_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Read volume group on node.\n\nReturns 404 if the node or volume group is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read volume group on node.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the node or volume group is not found.\nReturns 409 if the node is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume group on a node.",
+ "name": "VolumeGroupHandler",
+ "params": [
+ "system_id",
+ "volume_group_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{volume_group_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{volume_group_id}/"
+ },
+ "name": "VolumeGroupHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume groups on a node.",
+ "name": "VolumeGroupsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/"
+ },
+ "name": "VolumeGroupsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.",
+ "name": "ZoneHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/zones/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/"
+ },
+ "name": "ZoneHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List zones.\n\nGet a listing of all the physical zones.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage physical zones.",
+ "name": "ZonesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/zones/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/"
+ },
+ "name": "ZonesHandler"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/maas/client/bones/tests/api20.json b/maas/client/bones/testing/api20.raw.json
similarity index 100%
rename from maas/client/bones/tests/api20.json
rename to maas/client/bones/testing/api20.raw.json
diff --git a/maas/client/bones/testing/api21.json b/maas/client/bones/testing/api21.json
new file mode 100644
index 00000000..314b497f
--- /dev/null
+++ b/maas/client/bones/testing/api21.json
@@ -0,0 +1,3270 @@
+{
+ "doc": "MAAS API",
+ "hash": "503f93ea00f40fb77070f447466a1a557bdfd409",
+ "resources": [
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)",
+ "method": "POST",
+ "name": "create_authorisation_token",
+ "op": "create_authorisation_token",
+ "restful": false
+ },
+ {
+ "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode",
+ "method": "POST",
+ "name": "delete_authorisation_token",
+ "op": "delete_authorisation_token",
+ "restful": false
+ },
+ {
+ "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token.",
+ "method": "GET",
+ "name": "list_authorisation_tokens",
+ "op": "list_authorisation_tokens",
+ "restful": false
+ },
+ {
+ "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode",
+ "method": "POST",
+ "name": "update_token_name",
+ "op": "update_token_name",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the current logged-in user.",
+ "name": "AccountHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/"
+ },
+ "name": "AccountHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache set on a machine.",
+ "name": "BcacheCacheSetHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/"
+ },
+ "name": "BcacheCacheSetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache sets on a machine.",
+ "name": "BcacheCacheSetsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/"
+ },
+ "name": "BcacheCacheSetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache device on a machine.",
+ "name": "BcacheHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/"
+ },
+ "name": "BcacheHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache devices on a machine.",
+ "name": "BcachesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/"
+ },
+ "name": "BcachesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "add_tag",
+ "op": "add_tag",
+ "restful": false
+ },
+ {
+ "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "remove_tag",
+ "op": "remove_tag",
+ "restful": false
+ },
+ {
+ "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "set_boot_disk",
+ "op": "set_boot_disk",
+ "restful": false
+ },
+ {
+ "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ },
+ {
+ "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a block device on a machine.",
+ "name": "BlockDeviceHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/"
+ },
+ "name": "BlockDeviceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage block devices on a machine.",
+ "name": "BlockDevicesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/"
+ },
+ "name": "BlockDevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete boot resource.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot resource.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot resource.",
+ "name": "BootResourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-resources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/"
+ },
+ "name": "BootResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Import the boot resources.",
+ "method": "POST",
+ "name": "import",
+ "op": "import",
+ "restful": false
+ },
+ {
+ "doc": "Return import status.",
+ "method": "GET",
+ "name": "is_importing",
+ "op": "is_importing",
+ "restful": false
+ },
+ {
+ "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Stop import of boot resources.",
+ "method": "POST",
+ "name": "stop_import",
+ "op": "stop_import",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the boot resources.",
+ "name": "BootResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-resources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/"
+ },
+ "name": "BootResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source.",
+ "name": "BootSourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/"
+ },
+ "name": "BootSourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source selection.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source selection.",
+ "name": "BootSourceSelectionHandler",
+ "params": [
+ "boot_source_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/"
+ },
+ "name": "BootSourceSelectionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot source selections.\n\nGet a listing of a boot source's selections.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot source selections.",
+ "name": "BootSourceSelectionsHandler",
+ "params": [
+ "boot_source_id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/"
+ },
+ "name": "BootSourceSelectionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot sources.\n\nGet a listing of boot sources.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot sources.",
+ "name": "BootSourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-sources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/"
+ },
+ "name": "BootSourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a commissioning script.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a commissioning script.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a commissioning script.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/commissioning-scripts/{name}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}"
+ },
+ "name": "CommissioningScriptHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List commissioning scripts.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/commissioning-scripts/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/"
+ },
+ "name": "CommissioningScriptsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "POST",
+ "name": "revert",
+ "op": "revert",
+ "restful": false
+ },
+ {
+ "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.",
+ "name": "DHCPSnippetHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dhcp-snippets/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/"
+ },
+ "name": "DHCPSnippetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all DHCP snippets.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all DHCP snippets in MAAS.",
+ "name": "DHCPSnippetsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dhcp-snippets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/"
+ },
+ "name": "DHCPSnippetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresource.",
+ "name": "DNSResourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/"
+ },
+ "name": "DNSResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecord.",
+ "name": "DNSResourceRecordHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/"
+ },
+ "name": "DNSResourceRecordHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecords.",
+ "name": "DNSResourceRecordsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/"
+ },
+ "name": "DNSResourceRecordsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresources.",
+ "name": "DNSResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/"
+ },
+ "name": "DNSResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device.",
+ "method": "POST",
+ "name": "restore_default_configuration",
+ "op": "restore_default_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device.",
+ "method": "POST",
+ "name": "restore_networking_configuration",
+ "op": "restore_networking_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.",
+ "method": "POST",
+ "name": "set_owner_data",
+ "op": "set_owner_data",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual device.\n\nThe device is identified by its system_id.",
+ "name": "DeviceHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/devices/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/"
+ },
+ "name": "DeviceHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the devices in the MAAS.",
+ "name": "DevicesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/devices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/"
+ },
+ "name": "DevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_ip",
+ "op": "by_unknown_ip",
+ "restful": false
+ },
+ {
+ "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_ip_and_mac",
+ "op": "by_unknown_ip_and_mac",
+ "restful": false
+ },
+ {
+ "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_mac",
+ "op": "by_unknown_mac",
+ "restful": false
+ },
+ {
+ "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data.",
+ "method": "POST",
+ "name": "clear",
+ "op": "clear",
+ "restful": false
+ },
+ {
+ "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU.",
+ "method": "POST",
+ "name": "scan",
+ "op": "scan",
+ "restful": false
+ }
+ ],
+ "doc": "Query observed discoveries.",
+ "name": "DiscoveriesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/discovery/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/discovery/"
+ },
+ "name": "DiscoveriesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Read or delete an observed discovery.",
+ "name": "DiscoveryHandler",
+ "params": [
+ "discovery_id"
+ ],
+ "path": "/MAAS/api/2.0/discovery/{discovery_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/"
+ },
+ "name": "DiscoveryHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read domain.\n\nReturns 404 if the domain is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage domain.",
+ "name": "DomainHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/domains/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/"
+ },
+ "name": "DomainHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all domains.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next.",
+ "method": "POST",
+ "name": "set_serial",
+ "op": "set_serial",
+ "restful": false
+ }
+ ],
+ "doc": "Manage domains.",
+ "name": "DomainsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/domains/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/"
+ },
+ "name": "DomainsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events.",
+ "method": "GET",
+ "name": "query",
+ "op": "query",
+ "restful": false
+ }
+ ],
+ "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.",
+ "name": "EventsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/events/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/events/"
+ },
+ "name": "EventsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabric.",
+ "name": "FabricHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/"
+ },
+ "name": "FabricHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fabrics.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabrics.",
+ "name": "FabricsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fabrics/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/"
+ },
+ "name": "FabricsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Network.",
+ "name": "FanNetworkHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/fannetworks/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/"
+ },
+ "name": "FanNetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fannetworks.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Networks.",
+ "name": "FanNetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fannetworks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/"
+ },
+ "name": "FanNetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a FileStorage object.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.",
+ "name": "FileHandler",
+ "params": [
+ "filename"
+ ],
+ "path": "/MAAS/api/2.0/files/{filename}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/"
+ },
+ "name": "FileHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.",
+ "name": "AnonFilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get",
+ "op": "get",
+ "restful": false
+ },
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ },
+ {
+ "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the files in this MAAS.",
+ "name": "FilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "name": "FilesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available.",
+ "method": "POST",
+ "name": "reserve",
+ "op": "reserve",
+ "restful": false
+ }
+ ],
+ "doc": "Manage IP addresses allocated by MAAS.",
+ "name": "IPAddressesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/ipaddresses/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/"
+ },
+ "name": "IPAddressesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read IP range.\n\nReturns 404 if the IP range is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage IP range.",
+ "name": "IPRangeHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/ipranges/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/"
+ },
+ "name": "IPRangeHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all IP ranges.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage IP ranges.",
+ "name": "IPRangesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/ipranges/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/"
+ },
+ "name": "IPRangesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.",
+ "method": "POST",
+ "name": "add_tag",
+ "op": "add_tag",
+ "restful": false
+ },
+ {
+ "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "disconnect",
+ "op": "disconnect",
+ "restful": false
+ },
+ {
+ "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "link_subnet",
+ "op": "link_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.",
+ "method": "POST",
+ "name": "remove_tag",
+ "op": "remove_tag",
+ "restful": false
+ },
+ {
+ "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "set_default_gateway",
+ "op": "set_default_gateway",
+ "restful": false
+ },
+ {
+ "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "unlink_subnet",
+ "op": "unlink_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond-mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond-miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond-downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond-updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond-lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond-xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a node's or device's interface.",
+ "name": "InterfaceHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/"
+ },
+ "name": "InterfaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bond",
+ "op": "create_bond",
+ "restful": false
+ },
+ {
+ "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bridge",
+ "op": "create_bridge",
+ "restful": false
+ },
+ {
+ "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_physical",
+ "op": "create_physical",
+ "restful": false
+ },
+ {
+ "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_vlan",
+ "op": "create_vlan",
+ "restful": false
+ },
+ {
+ "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage interfaces on a node.",
+ "name": "InterfacesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/"
+ },
+ "name": "InterfacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete license key.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read license key.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a license key.",
+ "name": "LicenseKeyHandler",
+ "params": [
+ "osystem",
+ "distro_series"
+ ],
+ "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}"
+ },
+ "name": "LicenseKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List license keys.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the license keys.",
+ "name": "LicenseKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/license-keys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/"
+ },
+ "name": "LicenseKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)",
+ "method": "GET",
+ "name": "get_config",
+ "op": "get_config",
+ "restful": false
+ },
+ {
+ "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)",
+ "method": "POST",
+ "name": "set_config",
+ "op": "set_config",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the MAAS server.",
+ "name": "MaasHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/maas/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/maas/"
+ },
+ "name": "MaasHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Abort a machine's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nThis currently only supports aborting of the 'Disk Erasing' operation.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.",
+ "method": "POST",
+ "name": "abort",
+ "op": "abort",
+ "restful": false
+ },
+ {
+ "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways.",
+ "method": "POST",
+ "name": "clear_default_gateways",
+ "op": "clear_default_gateways",
+ "restful": false
+ },
+ {
+ "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found.",
+ "method": "POST",
+ "name": "commission",
+ "op": "commission",
+ "restful": false
+ },
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "deploy",
+ "op": "deploy",
+ "restful": false
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine.",
+ "method": "POST",
+ "name": "exit_rescue_mode",
+ "op": "exit_rescue_mode",
+ "restful": false
+ },
+ {
+ "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration.",
+ "method": "GET",
+ "name": "get_curtin_config",
+ "op": "get_curtin_config",
+ "restful": false
+ },
+ {
+ "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.",
+ "method": "POST",
+ "name": "mark_broken",
+ "op": "mark_broken",
+ "restful": false
+ },
+ {
+ "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed.",
+ "method": "POST",
+ "name": "mark_fixed",
+ "op": "mark_fixed",
+ "restful": false
+ },
+ {
+ "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition.",
+ "method": "POST",
+ "name": "mount_special",
+ "op": "mount_special",
+ "restful": false
+ },
+ {
+ "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.",
+ "method": "POST",
+ "name": "power_off",
+ "op": "power_off",
+ "restful": false
+ },
+ {
+ "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "power_on",
+ "op": "power_on",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.",
+ "method": "GET",
+ "name": "query_power_state",
+ "op": "query_power_state",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine.",
+ "method": "POST",
+ "name": "rescue_mode",
+ "op": "rescue_mode",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_default_configuration",
+ "op": "restore_default_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_networking_configuration",
+ "op": "restore_networking_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_storage_configuration",
+ "op": "restore_storage_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.",
+ "method": "POST",
+ "name": "set_owner_data",
+ "op": "set_owner_data",
+ "restful": false
+ },
+ {
+ "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout.",
+ "method": "POST",
+ "name": "set_storage_layout",
+ "op": "set_storage_layout",
+ "restful": false
+ },
+ {
+ "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition.",
+ "method": "POST",
+ "name": "unmount_special",
+ "op": "unmount_special",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.",
+ "name": "MachineHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/machines/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/"
+ },
+ "name": "MachineHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Machines.",
+ "name": "AnonMachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.",
+ "method": "POST",
+ "name": "accept_all",
+ "op": "accept_all",
+ "restful": false
+ },
+ {
+ "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed.",
+ "method": "POST",
+ "name": "add_chassis",
+ "op": "add_chassis",
+ "restful": false
+ },
+ {
+ "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound.",
+ "method": "POST",
+ "name": "allocate",
+ "op": "allocate",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Fetch Machines that were allocated to the User/oauth token.",
+ "method": "GET",
+ "name": "list_allocated",
+ "op": "list_allocated",
+ "restful": false
+ },
+ {
+ "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the machines in the MAAS.",
+ "name": "MachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "name": "MachinesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "connect_macs",
+ "op": "connect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "disconnect_macs",
+ "op": "disconnect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned.",
+ "method": "GET",
+ "name": "list_connected_macs",
+ "op": "list_connected_macs",
+ "restful": false
+ },
+ {
+ "doc": "Read network definition.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.",
+ "name": "NetworkHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/networks/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/"
+ },
+ "name": "NetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.",
+ "name": "NetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/networks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/"
+ },
+ "name": "NetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.",
+ "name": "NodeHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/"
+ },
+ "name": "NodeHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Read the collection of NodeResult in the MAAS.",
+ "name": "NodeResultsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/installation-results/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/"
+ },
+ "name": "NodeResultsHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the nodes in the MAAS.",
+ "name": "NodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "name": "NodesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all Package Repositories.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all Package Repositories in MAAS.",
+ "name": "PackageRepositoriesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/package-repositories/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/"
+ },
+ "name": "PackageRepositoriesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.",
+ "name": "PackageRepositoryHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/package-repositories/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/"
+ },
+ "name": "PackageRepositoryHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Unformat a partition.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ }
+ ],
+ "doc": "Manage partition on a block device.",
+ "name": "PartitionHandler",
+ "params": [
+ "system_id",
+ "device_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}"
+ },
+ "name": "PartitionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage partitions on a block device.",
+ "name": "PartitionsHandler",
+ "params": [
+ "system_id",
+ "device_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/"
+ },
+ "name": "PartitionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found.",
+ "method": "POST",
+ "name": "import_boot_images",
+ "op": "import_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found.",
+ "method": "GET",
+ "name": "list_boot_images",
+ "op": "list_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.",
+ "method": "POST",
+ "name": "power_off",
+ "op": "power_off",
+ "restful": false
+ },
+ {
+ "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "power_on",
+ "op": "power_on",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.",
+ "method": "GET",
+ "name": "query_power_state",
+ "op": "query_power_state",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.",
+ "name": "RackControllerHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/"
+ },
+ "name": "RackControllerHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format.",
+ "method": "GET",
+ "name": "describe_power_types",
+ "op": "describe_power_types",
+ "restful": false
+ },
+ {
+ "doc": "Import the boot images on all rack controllers.",
+ "method": "POST",
+ "name": "import_boot_images",
+ "op": "import_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all rack controllers in MAAS.",
+ "name": "RackControllersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/rackcontrollers/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/"
+ },
+ "name": "RackControllersHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a specific RAID device on a machine.",
+ "name": "RaidHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/"
+ },
+ "name": "RaidHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage all RAID devices on a machine.",
+ "name": "RaidsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raids/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/"
+ },
+ "name": "RaidsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.",
+ "name": "RegionControllerHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/"
+ },
+ "name": "RegionControllerHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all region controllers in MAAS.",
+ "name": "RegionControllersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/regioncontrollers/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/"
+ },
+ "name": "RegionControllersHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.",
+ "name": "SSHKeyHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/"
+ },
+ "name": "SSHKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format.",
+ "method": "POST",
+ "name": "import",
+ "op": "import",
+ "restful": false
+ },
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the SSH keys in this MAAS.",
+ "name": "SSHKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/"
+ },
+ "name": "SSHKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.",
+ "name": "SSLKeyHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/"
+ },
+ "name": "SSLKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Operations on multiple keys.",
+ "name": "SSLKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/"
+ },
+ "name": "SSLKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete space.\n\nReturns 404 if the space is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read space.\n\nReturns 404 if the space is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage space.",
+ "name": "SpaceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/spaces/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/"
+ },
+ "name": "SpaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all spaces.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage spaces.",
+ "name": "SpacesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/spaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/"
+ },
+ "name": "SpacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete static route.\n\nReturns 404 if the static route is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read static route.\n\nReturns 404 if the static route is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage static route.",
+ "name": "StaticRouteHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/static-routes/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/"
+ },
+ "name": "StaticRouteHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all static routes.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage static routes.",
+ "name": "StaticRoutesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/static-routes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/"
+ },
+ "name": "StaticRoutesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional arguments:\nwith_username: (default=True) if False, suppresses the display\nof usernames associated with each address.\nwith_node_summary: (default=True) if False, suppresses the display\nof any node associated with each address.",
+ "method": "GET",
+ "name": "ip_addresses",
+ "op": "ip_addresses",
+ "restful": false
+ },
+ {
+ "doc": "Read subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "reserved_ip_ranges",
+ "op": "reserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Returns statistics for the specified subnet, including:\n\nnum_available - the number of available IP addresses\nlargest_available - the largest number of contiguous free IP addresses\nnum_unavailable - the number of unavailable IP addresses\ntotal_addresses - the sum of the available plus unavailable addresses\nusage - the (floating point) usage percentage of this subnet\nusage_string - the (formatted unicode) usage percentage of this subnet\nranges - the specific IP ranges present in ths subnet (if specified)\n\nOptional arguments:\ninclude_ranges: if True, includes detailed information\nabout the usage of this range.\ninclude_suggestions: if True, includes the suggested gateway and\ndynamic range for this subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "statistics",
+ "op": "statistics",
+ "restful": false
+ },
+ {
+ "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "unreserved_ip_ranges",
+ "op": "unreserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Update subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param vlan: VLAN this subnet belongs to.\n:param space: Space this subnet is in.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n:param allow_proxy: Configure maas-proxy to allow requests from this subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnet.",
+ "name": "SubnetHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/subnets/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/"
+ },
+ "name": "SubnetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param fabric: Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to or defaults to the default fabric.\n:param vlan: VLAN this subnet belongs to. Defaults to the default\n VLAN for the provided fabric or defaults to the default VLAN in\n the default fabric.\n:param vid: VID of the VLAN this subnet belongs to. Only used when\n vlan is not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n:param space: Space this subnet is in. Defaults to the default space.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means\n no reverse zone is created; Enabled means generate the reverse\n zone; RFC2317 extends Enabled to create the necessary parent zone\n with the appropriate CNAME resource records for the network, if the\n network is small enough to require the support described in\n RFC2317.\n:param allow_proxy: Configure maas-proxy to allow requests from this\n subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this\n subnet.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all subnets.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnets.",
+ "name": "SubnetsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/subnets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/"
+ },
+ "name": "SubnetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "devices",
+ "op": "devices",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "machines",
+ "op": "machines",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "nodes",
+ "op": "nodes",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "rack_controllers",
+ "op": "rack_controllers",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found.",
+ "method": "POST",
+ "name": "rebuild",
+ "op": "rebuild",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "region_controllers",
+ "op": "region_controllers",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition.",
+ "method": "POST",
+ "name": "update_nodes",
+ "op": "update_nodes",
+ "restful": false
+ }
+ ],
+ "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.",
+ "name": "TagHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/tags/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/"
+ },
+ "name": "TagHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List Tags.\n\nGet a listing of all tags that are currently defined.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the Tags in this MAAS.",
+ "name": "TagsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/tags/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/"
+ },
+ "name": "TagsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Deletes a user",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a user account.",
+ "name": "UserHandler",
+ "params": [
+ "username"
+ ],
+ "path": "/MAAS/api/2.0/users/{username}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/"
+ },
+ "name": "UserHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List users.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns the currently logged in user.",
+ "method": "GET",
+ "name": "whoami",
+ "op": "whoami",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the user accounts of this MAAS.",
+ "name": "UsersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/users/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/users/"
+ },
+ "name": "UsersHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Version and capabilities of this MAAS instance.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }",
+ "name": "VersionHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/version/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/version/"
+ },
+ "auth": null,
+ "name": "VersionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:Param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLAN on a fabric.",
+ "name": "VlanHandler",
+ "params": [
+ "fabric_id",
+ "vid"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/"
+ },
+ "name": "VlanHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLANs on a fabric.",
+ "name": "VlansHandler",
+ "params": [
+ "fabric_id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/"
+ },
+ "name": "VlansHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create_logical_volume",
+ "op": "create_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "delete_logical_volume",
+ "op": "delete_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume group on a machine.",
+ "name": "VolumeGroupHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/"
+ },
+ "name": "VolumeGroupHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume groups on a machine.",
+ "name": "VolumeGroupsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/"
+ },
+ "name": "VolumeGroupsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.",
+ "name": "ZoneHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/zones/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/"
+ },
+ "name": "ZoneHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List zones.\n\nGet a listing of all the physical zones.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage physical zones.",
+ "name": "ZonesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/zones/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/"
+ },
+ "name": "ZonesHandler"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/maas/client/bones/testing/api21.raw.json b/maas/client/bones/testing/api21.raw.json
new file mode 100644
index 00000000..640b4fea
--- /dev/null
+++ b/maas/client/bones/testing/api21.raw.json
@@ -0,0 +1 @@
+{"resources": [{"anon": null, "name": "RaidsHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/raids/", "doc": "Manage all RAID devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/", "name": "RaidsHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/machines/", "doc": "Anonymous access to Machines.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode"}, {"restful": false, "name": "accept", "method": "POST", "op": "accept", "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401."}, {"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "name": "AnonMachinesHandler"}, "name": "MachinesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/machines/", "doc": "Manage the collection of all the machines in the MAAS.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "allocate", "method": "POST", "op": "allocate", "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound."}, {"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "add_chassis", "method": "POST", "op": "add_chassis", "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed."}, {"restful": false, "name": "accept_all", "method": "POST", "op": "accept_all", "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result."}, {"restful": false, "name": "list_allocated", "method": "GET", "op": "list_allocated", "doc": "Fetch Machines that were allocated to the User/oauth token."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}, {"restful": false, "name": "accept", "method": "POST", "op": "accept", "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "name": "MachinesHandler"}}, {"anon": null, "name": "SubnetHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/subnets/{id}/", "doc": "Manage subnet.", "actions": [{"restful": false, "name": "unreserved_ip_ranges", "method": "GET", "op": "unreserved_ip_ranges", "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param vlan: VLAN this subnet belongs to.\n:param space: Space this subnet is in.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n:param allow_proxy: Configure maas-proxy to allow requests from this subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "statistics", "method": "GET", "op": "statistics", "doc": "Returns statistics for the specified subnet, including:\n\nnum_available - the number of available IP addresses\nlargest_available - the largest number of contiguous free IP addresses\nnum_unavailable - the number of unavailable IP addresses\ntotal_addresses - the sum of the available plus unavailable addresses\nusage - the (floating point) usage percentage of this subnet\nusage_string - the (formatted unicode) usage percentage of this subnet\nranges - the specific IP ranges present in ths subnet (if specified)\n\nOptional arguments:\ninclude_ranges: if True, includes detailed information\nabout the usage of this range.\ninclude_suggestions: if True, includes the suggested gateway and\ndynamic range for this subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "reserved_ip_ranges", "method": "GET", "op": "reserved_ip_ranges", "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "ip_addresses", "method": "GET", "op": "ip_addresses", "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional arguments:\nwith_username: (default=True) if False, suppresses the display\nof usernames associated with each address.\nwith_node_summary: (default=True) if False, suppresses the display\nof any node associated with each address."}], "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/", "name": "SubnetHandler"}}, {"anon": null, "name": "DNSResourceRecordsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/dnsresourcerecords/", "doc": "Manage dnsresourcerecords.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/", "name": "DNSResourceRecordsHandler"}}, {"anon": null, "name": "LicenseKeyHandler", "auth": {"params": ["osystem", "distro_series"], "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "doc": "Manage a license key.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete license key."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read license key."}], "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "name": "LicenseKeyHandler"}}, {"anon": null, "name": "RaidHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "doc": "Manage a specific RAID device on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "name": "RaidHandler"}}, {"anon": null, "name": "BootResourcesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/boot-resources/", "doc": "Manage the boot resources.", "actions": [{"restful": false, "name": "import", "method": "POST", "op": "import", "doc": "Import the boot resources."}, {"restful": false, "name": "stop_import", "method": "POST", "op": "stop_import", "doc": "Stop import of boot resources."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload."}, {"restful": false, "name": "is_importing", "method": "GET", "op": "is_importing", "doc": "Return import status."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all"}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/", "name": "BootResourcesHandler"}}, {"anon": null, "name": "BcacheHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "doc": "Manage bcache device on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "name": "BcacheHandler"}}, {"anon": null, "name": "RackControllerHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/", "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "list_boot_images", "method": "GET", "op": "list_boot_images", "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "power_on", "method": "POST", "op": "power_on", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "import_boot_images", "method": "POST", "op": "import_boot_images", "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found."}, {"restful": false, "name": "query_power_state", "method": "GET", "op": "query_power_state", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller."}, {"restful": false, "name": "power_off", "method": "POST", "op": "power_off", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node."}], "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/", "name": "RackControllerHandler"}}, {"anon": null, "name": "UserHandler", "auth": {"params": ["username"], "path": "/MAAS/api/2.0/users/{username}/", "doc": "Manage a user account.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Deletes a user"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": null}], "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/", "name": "UserHandler"}}, {"anon": null, "name": "SubnetsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/subnets/", "doc": "Manage subnets.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param fabric: Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to or defaults to the default fabric.\n:param vlan: VLAN this subnet belongs to. Defaults to the default\n VLAN for the provided fabric or defaults to the default VLAN in\n the default fabric.\n:param vid: VID of the VLAN this subnet belongs to. Only used when\n vlan is not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n:param space: Space this subnet is in. Defaults to the default space.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means\n no reverse zone is created; Enabled means generate the reverse\n zone; RFC2317 extends Enabled to create the necessary parent zone\n with the appropriate CNAME resource records for the network, if the\n network is small enough to require the support described in\n RFC2317.\n:param allow_proxy: Configure maas-proxy to allow requests from this\n subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this\n subnet."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all subnets."}], "uri": "http://localhost:5240/MAAS/api/2.0/subnets/", "name": "SubnetsHandler"}}, {"anon": null, "name": "DNSResourceHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/dnsresources/{id}/", "doc": "Manage dnsresource.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/", "name": "DNSResourceHandler"}}, {"anon": null, "name": "DiscoveryHandler", "auth": {"params": ["discovery_id"], "path": "/MAAS/api/2.0/discovery/{discovery_id}/", "doc": "Read or delete an observed discovery.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": null}], "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/", "name": "DiscoveryHandler"}}, {"anon": null, "name": "SSLKeysHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/account/prefs/sslkeys/", "doc": "Operations on multiple keys.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\"."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all keys belonging to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/", "name": "SSLKeysHandler"}}, {"anon": null, "name": "LicenseKeysHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/license-keys/", "doc": "Manage the license keys.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List license keys."}], "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/", "name": "LicenseKeysHandler"}}, {"anon": null, "name": "SSLKeyHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "name": "SSLKeyHandler"}}, {"anon": null, "name": "BcachesHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/", "doc": "Manage bcache devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/", "name": "BcachesHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, "name": "RackControllersHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/rackcontrollers/", "doc": "Manage the collection of all rack controllers in MAAS.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "describe_power_types", "method": "GET", "op": "describe_power_types", "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "import_boot_images", "method": "POST", "op": "import_boot_images", "doc": "Import the boot images on all rack controllers."}], "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/", "name": "RackControllersHandler"}}, {"anon": null, "name": "MaasHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/maas/", "doc": "Manage the MAAS server.", "actions": [{"restful": false, "name": "set_config", "method": "POST", "op": "set_config", "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)"}, {"restful": false, "name": "get_config", "method": "GET", "op": "get_config", "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)"}], "uri": "http://localhost:5240/MAAS/api/2.0/maas/", "name": "MaasHandler"}}, {"anon": null, "name": "DiscoveriesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/discovery/", "doc": "Query observed discoveries.", "actions": [{"restful": false, "name": "scan", "method": "POST", "op": "scan", "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU."}, {"restful": false, "name": "by_unknown_ip", "method": "GET", "op": "by_unknown_ip", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": false, "name": "by_unknown_ip_and_mac", "method": "GET", "op": "by_unknown_ip_and_mac", "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": false, "name": "clear", "method": "POST", "op": "clear", "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data."}, {"restful": false, "name": "by_unknown_mac", "method": "GET", "op": "by_unknown_mac", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}], "uri": "http://localhost:5240/MAAS/api/2.0/discovery/", "name": "DiscoveriesHandler"}}, {"anon": null, "name": "IPRangeHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/ipranges/{id}/", "doc": "Manage IP range.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read IP range.\n\nReturns 404 if the IP range is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/", "name": "IPRangeHandler"}}, {"anon": null, "name": "DNSResourcesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/dnsresources/", "doc": "Manage dnsresources.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/", "name": "DNSResourcesHandler"}}, {"anon": null, "name": "BootSourceSelectionsHandler", "auth": {"params": ["boot_source_id"], "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "doc": "Manage the collection of boot source selections.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List boot source selections.\n\nGet a listing of a boot source's selections."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "name": "BootSourceSelectionsHandler"}}, {"anon": null, "name": "BcacheCacheSetHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "doc": "Manage bcache cache set on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "name": "BcacheCacheSetHandler"}}, {"anon": null, "name": "RegionControllerHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/", "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/", "name": "RegionControllerHandler"}}, {"anon": null, "name": "EventsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/events/", "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.", "actions": [{"restful": false, "name": "query", "method": "GET", "op": "query", "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events."}], "uri": "http://localhost:5240/MAAS/api/2.0/events/", "name": "EventsHandler"}}, {"anon": null, "name": "IPRangesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/ipranges/", "doc": "Manage IP ranges.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all IP ranges."}], "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/", "name": "IPRangesHandler"}}, {"anon": null, "name": "DomainHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/domains/{id}/", "doc": "Manage domain.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read domain.\n\nReturns 404 if the domain is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/", "name": "DomainHandler"}}, {"anon": null, "name": "BcacheCacheSetsHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "doc": "Manage bcache cache sets on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "name": "BcacheCacheSetsHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, "name": "RegionControllersHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/regioncontrollers/", "doc": "Manage the collection of all region controllers in MAAS.", "actions": [{"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/", "name": "RegionControllersHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/files/", "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.", "actions": [{"restful": false, "name": "get_by_key", "method": "GET", "op": "get_by_key", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/", "name": "AnonFilesHandler"}, "name": "FilesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/files/", "doc": "Manage the collection of all the files in this MAAS.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string"}, {"restful": false, "name": "get_by_key", "method": "GET", "op": "get_by_key", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode"}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied"}, {"restful": false, "name": "get", "method": "GET", "op": "get", "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/", "name": "FilesHandler"}}, {"anon": null, "name": "NodeResultsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/installation-results/", "doc": "Read the collection of NodeResult in the MAAS.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable"}], "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/", "name": "NodeResultsHandler"}}, {"anon": null, "name": "StaticRouteHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/static-routes/{id}/", "doc": "Manage static route.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete static route.\n\nReturns 404 if the static route is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read static route.\n\nReturns 404 if the static route is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/", "name": "StaticRouteHandler"}}, {"anon": null, "name": "DomainsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/domains/", "doc": "Manage domains.", "actions": [{"restful": false, "name": "set_serial", "method": "POST", "op": "set_serial", "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all domains."}], "uri": "http://localhost:5240/MAAS/api/2.0/domains/", "name": "DomainsHandler"}}, {"anon": null, "name": "TagsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/tags/", "doc": "Manage the collection of all the Tags in this MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Tags.\n\nGet a listing of all tags that are currently defined."}], "uri": "http://localhost:5240/MAAS/api/2.0/tags/", "name": "TagsHandler"}}, {"anon": null, "name": "InterfaceHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "doc": "Manage a node's or device's interface.", "actions": [{"restful": false, "name": "add_tag", "method": "POST", "op": "add_tag", "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface."}, {"restful": false, "name": "unlink_subnet", "method": "POST", "op": "unlink_subnet", "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond-mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond-miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond-downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond-updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond-lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond-xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "set_default_gateway", "method": "POST", "op": "set_default_gateway", "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "disconnect", "method": "POST", "op": "disconnect", "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "remove_tag", "method": "POST", "op": "remove_tag", "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "link_subnet", "method": "POST", "op": "link_subnet", "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "name": "InterfaceHandler"}}, {"anon": null, "name": "DeviceHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/devices/{system_id}/", "doc": "Manage an individual device.\n\nThe device is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device."}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "op": "restore_networking_configuration", "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device."}, {"restful": false, "name": "set_owner_data", "method": "POST", "op": "set_owner_data", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "op": "restore_default_configuration", "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device."}], "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/", "name": "DeviceHandler"}}, {"anon": null, "name": "BlockDevicesHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "doc": "Manage block devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "name": "BlockDevicesHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/version/", "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Version and capabilities of this MAAS instance."}], "uri": "http://localhost:5240/MAAS/api/2.0/version/", "name": "VersionHandler"}, "name": "VersionHandler", "auth": null}, {"anon": null, "name": "FileHandler", "auth": {"params": ["filename"], "path": "/MAAS/api/2.0/files/{filename}/", "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a FileStorage object."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/", "name": "FileHandler"}}, {"anon": null, "name": "StaticRoutesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/static-routes/", "doc": "Manage static routes.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all static routes."}], "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/", "name": "StaticRoutesHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, "name": "DevicesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/devices/", "doc": "Manage the collection of all the devices in the MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/devices/", "name": "DevicesHandler"}}, {"anon": null, "name": "FabricHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/fabrics/{id}/", "doc": "Manage fabric.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read fabric.\n\nReturns 404 if the fabric is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/", "name": "FabricHandler"}}, {"anon": null, "name": "BlockDeviceHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "doc": "Manage a block device on a machine.", "actions": [{"restful": false, "name": "add_tag", "method": "POST", "op": "add_tag", "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "format", "method": "POST", "op": "format", "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "unmount", "method": "POST", "op": "unmount", "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": false, "name": "unformat", "method": "POST", "op": "unformat", "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found."}, {"restful": false, "name": "remove_tag", "method": "POST", "op": "remove_tag", "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "mount", "method": "POST", "op": "mount", "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": false, "name": "set_boot_disk", "method": "POST", "op": "set_boot_disk", "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "name": "BlockDeviceHandler"}}, {"anon": null, "name": "IPAddressesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/ipaddresses/", "doc": "Manage IP addresses allocated by MAAS.", "actions": [{"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found."}, {"restful": false, "name": "reserve", "method": "POST", "op": "reserve", "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/", "name": "IPAddressesHandler"}}, {"anon": null, "name": "CommissioningScriptsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/commissioning-scripts/", "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List commissioning scripts."}], "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/", "name": "CommissioningScriptsHandler"}}, {"anon": null, "name": "CommissioningScriptHandler", "auth": {"params": ["name"], "path": "/MAAS/api/2.0/commissioning-scripts/{name}", "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a commissioning script."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a commissioning script."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a commissioning script."}], "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}", "name": "CommissioningScriptHandler"}}, {"anon": null, "name": "FabricsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/fabrics/", "doc": "Manage fabrics.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all fabrics."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/", "name": "FabricsHandler"}}, {"anon": null, "name": "DHCPSnippetHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/dhcp-snippets/{id}/", "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": false, "name": "revert", "method": "POST", "op": "revert", "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/", "name": "DHCPSnippetHandler"}}, {"anon": null, "name": "PartitionHandler", "auth": {"params": ["system_id", "device_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "doc": "Manage partition on a block device.", "actions": [{"restful": false, "name": "format", "method": "POST", "op": "format", "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": false, "name": "unformat", "method": "POST", "op": "unformat", "doc": "Unformat a partition."}, {"restful": false, "name": "unmount", "method": "POST", "op": "unmount", "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found."}, {"restful": false, "name": "mount", "method": "POST", "op": "mount", "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "name": "PartitionHandler"}}, {"anon": null, "name": "NetworkHandler", "auth": {"params": ["name"], "path": "/MAAS/api/2.0/networks/{name}/", "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.", "actions": [{"restful": false, "name": "list_connected_macs", "method": "GET", "op": "list_connected_macs", "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": false, "name": "connect_macs", "method": "POST", "op": "connect_macs", "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": false, "name": "disconnect_macs", "method": "POST", "op": "disconnect_macs", "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read network definition."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators."}], "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/", "name": "NetworkHandler"}}, {"anon": null, "name": "TagHandler", "auth": {"params": ["name"], "path": "/MAAS/api/2.0/tags/{name}/", "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.", "actions": [{"restful": false, "name": "update_nodes", "method": "POST", "op": "update_nodes", "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "machines", "method": "GET", "op": "machines", "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "region_controllers", "method": "GET", "op": "region_controllers", "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "rack_controllers", "method": "GET", "op": "rack_controllers", "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "rebuild", "method": "POST", "op": "rebuild", "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "devices", "method": "GET", "op": "devices", "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "nodes", "method": "GET", "op": "nodes", "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted."}], "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/", "name": "TagHandler"}}, {"anon": null, "name": "SSHKeysHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/account/prefs/sshkeys/", "doc": "Manage the collection of all the SSH keys in this MAAS.", "actions": [{"restful": false, "name": "import", "method": "POST", "op": "import", "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\"."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all keys belonging to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/", "name": "SSHKeysHandler"}}, {"anon": null, "name": "ZonesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/zones/", "doc": "Manage physical zones.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List zones.\n\nGet a listing of all the physical zones."}], "uri": "http://localhost:5240/MAAS/api/2.0/zones/", "name": "ZonesHandler"}}, {"anon": null, "name": "ZoneHandler", "auth": {"params": ["name"], "path": "/MAAS/api/2.0/zones/{name}/", "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/", "name": "ZoneHandler"}}, {"anon": null, "name": "FanNetworkHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/fannetworks/{id}/", "doc": "Manage Fan Network.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/", "name": "FanNetworkHandler"}}, {"anon": null, "name": "SSHKeyHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "name": "SSHKeyHandler"}}, {"anon": null, "name": "FanNetworksHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/fannetworks/", "doc": "Manage Fan Networks.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all fannetworks."}], "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/", "name": "FanNetworksHandler"}}, {"anon": null, "name": "DHCPSnippetsHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/dhcp-snippets/", "doc": "Manage the collection of all DHCP snippets in MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all DHCP snippets."}], "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/", "name": "DHCPSnippetsHandler"}}, {"anon": null, "name": "PartitionsHandler", "auth": {"params": ["system_id", "device_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "doc": "Manage partitions on a block device.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "name": "PartitionsHandler"}}, {"anon": null, "name": "NetworksHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/networks/", "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common."}], "uri": "http://localhost:5240/MAAS/api/2.0/networks/", "name": "NetworksHandler"}}, {"anon": null, "name": "BootSourceHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/boot-sources/{id}/", "doc": "Manage a boot source.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific boot source."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot source."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/", "name": "BootSourceHandler"}}, {"anon": null, "name": "InterfacesHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/", "doc": "Manage interfaces on a node.", "actions": [{"restful": false, "name": "create_physical", "method": "POST", "op": "create_physical", "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_bridge", "method": "POST", "op": "create_bridge", "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_vlan", "method": "POST", "op": "create_vlan", "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_bond", "method": "POST", "op": "create_bond", "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/", "name": "InterfacesHandler"}}, {"anon": null, "name": "VlanHandler", "auth": {"params": ["fabric_id", "vid"], "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "doc": "Manage VLAN on a fabric.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:Param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n\nReturns 404 if the fabric or VLAN is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "name": "VlanHandler"}}, {"anon": null, "name": "PackageRepositoryHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/package-repositories/{id}/", "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/", "name": "PackageRepositoryHandler"}}, {"anon": null, "name": "VolumeGroupHandler", "auth": {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "doc": "Manage volume group on a machine.", "actions": [{"restful": false, "name": "create_logical_volume", "method": "POST", "op": "create_logical_volume", "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "delete_logical_volume", "method": "POST", "op": "delete_logical_volume", "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "name": "VolumeGroupHandler"}}, {"anon": null, "name": "NodeHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/", "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/", "name": "NodeHandler"}}, {"anon": null, "name": "UsersHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/users/", "doc": "Manage the user accounts of this MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing."}, {"restful": false, "name": "whoami", "method": "GET", "op": "whoami", "doc": "Returns the currently logged in user."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List users."}], "uri": "http://localhost:5240/MAAS/api/2.0/users/", "name": "UsersHandler"}}, {"anon": null, "name": "VlansHandler", "auth": {"params": ["fabric_id"], "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "doc": "Manage VLANs on a fabric.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "name": "VlansHandler"}}, {"anon": null, "name": "BootSourcesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/boot-sources/", "doc": "Manage the collection of boot sources.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List boot sources.\n\nGet a listing of boot sources."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/", "name": "BootSourcesHandler"}}, {"anon": null, "name": "SpaceHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/spaces/{id}/", "doc": "Manage space.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete space.\n\nReturns 404 if the space is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read space.\n\nReturns 404 if the space is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/", "name": "SpaceHandler"}}, {"anon": null, "name": "BootResourceHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/boot-resources/{id}/", "doc": "Manage a boot resource.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete boot resource."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot resource."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/", "name": "BootResourceHandler"}}, {"anon": null, "name": "VolumeGroupsHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "doc": "Manage volume groups on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "name": "VolumeGroupsHandler"}}, {"anon": {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, "name": "NodesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Manage the collection of all the nodes in the MAAS.", "actions": [{"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "NodesHandler"}}, {"anon": null, "name": "AccountHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/account/", "doc": "Manage the current logged-in user.", "actions": [{"restful": false, "name": "delete_authorisation_token", "method": "POST", "op": "delete_authorisation_token", "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode"}, {"restful": false, "name": "create_authorisation_token", "method": "POST", "op": "create_authorisation_token", "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)"}, {"restful": false, "name": "update_token_name", "method": "POST", "op": "update_token_name", "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode"}, {"restful": false, "name": "list_authorisation_tokens", "method": "GET", "op": "list_authorisation_tokens", "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/", "name": "AccountHandler"}}, {"anon": null, "name": "PackageRepositoriesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/package-repositories/", "doc": "Manage the collection of all Package Repositories in MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all Package Repositories."}], "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/", "name": "PackageRepositoriesHandler"}}, {"anon": null, "name": "BootSourceSelectionHandler", "auth": {"params": ["boot_source_id", "id"], "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "doc": "Manage a boot source selection.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific boot source."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot source selection."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "name": "BootSourceSelectionHandler"}}, {"anon": null, "name": "DNSResourceRecordHandler", "auth": {"params": ["id"], "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/", "doc": "Manage dnsresourcerecord.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/", "name": "DNSResourceRecordHandler"}}, {"anon": null, "name": "SpacesHandler", "auth": {"params": [], "path": "/MAAS/api/2.0/spaces/", "doc": "Manage spaces.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all spaces."}], "uri": "http://localhost:5240/MAAS/api/2.0/spaces/", "name": "SpacesHandler"}}, {"anon": null, "name": "MachineHandler", "auth": {"params": ["system_id"], "path": "/MAAS/api/2.0/machines/{system_id}/", "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "mark_broken", "method": "POST", "op": "mark_broken", "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken."}, {"restful": false, "name": "unmount_special", "method": "POST", "op": "unmount_special", "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition."}, {"restful": false, "name": "commission", "method": "POST", "op": "commission", "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found."}, {"restful": false, "name": "get_curtin_config", "method": "GET", "op": "get_curtin_config", "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration."}, {"restful": false, "name": "deploy", "method": "POST", "op": "deploy", "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "clear_default_gateways", "method": "POST", "op": "clear_default_gateways", "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways."}, {"restful": false, "name": "query_power_state", "method": "GET", "op": "query_power_state", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state."}, {"restful": false, "name": "mark_fixed", "method": "POST", "op": "mark_fixed", "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed."}, {"restful": false, "name": "abort", "method": "POST", "op": "abort", "doc": "Abort a machine's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nThis currently only supports aborting of the 'Disk Erasing' operation.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation."}, {"restful": false, "name": "restore_storage_configuration", "method": "POST", "op": "restore_storage_configuration", "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine."}, {"restful": false, "name": "rescue_mode", "method": "POST", "op": "rescue_mode", "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine."}, {"restful": false, "name": "set_storage_layout", "method": "POST", "op": "set_storage_layout", "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout."}, {"restful": false, "name": "power_on", "method": "POST", "op": "power_on", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released."}, {"restful": false, "name": "exit_rescue_mode", "method": "POST", "op": "exit_rescue_mode", "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine."}, {"restful": false, "name": "power_off", "method": "POST", "op": "power_off", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node."}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "op": "restore_networking_configuration", "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": false, "name": "set_owner_data", "method": "POST", "op": "set_owner_data", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "op": "restore_default_configuration", "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": false, "name": "mount_special", "method": "POST", "op": "mount_special", "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/", "name": "MachineHandler"}}], "handlers": [{"params": [], "path": "/MAAS/api/2.0/machines/", "doc": "Anonymous access to Machines.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode"}, {"restful": false, "name": "accept", "method": "POST", "op": "accept", "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401."}, {"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "name": "AnonMachinesHandler"}, {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, {"params": [], "path": "/MAAS/api/2.0/files/", "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.", "actions": [{"restful": false, "name": "get_by_key", "method": "GET", "op": "get_by_key", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/", "name": "AnonFilesHandler"}, {"params": [], "path": "/MAAS/api/2.0/version/", "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Version and capabilities of this MAAS instance."}], "uri": "http://localhost:5240/MAAS/api/2.0/version/", "name": "VersionHandler"}, {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Anonymous access to Nodes.", "actions": [{"restful": false, "name": "is_registered", "method": "GET", "op": "is_registered", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "AnonNodesHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/raids/", "doc": "Manage all RAID devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/", "name": "RaidsHandler"}, {"params": [], "path": "/MAAS/api/2.0/machines/", "doc": "Manage the collection of all the machines in the MAAS.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "allocate", "method": "POST", "op": "allocate", "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound."}, {"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "add_chassis", "method": "POST", "op": "add_chassis", "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed."}, {"restful": false, "name": "accept_all", "method": "POST", "op": "accept_all", "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result."}, {"restful": false, "name": "list_allocated", "method": "GET", "op": "list_allocated", "doc": "Fetch Machines that were allocated to the User/oauth token."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}, {"restful": false, "name": "accept", "method": "POST", "op": "accept", "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "name": "MachinesHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/subnets/{id}/", "doc": "Manage subnet.", "actions": [{"restful": false, "name": "unreserved_ip_ranges", "method": "GET", "op": "unreserved_ip_ranges", "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param vlan: VLAN this subnet belongs to.\n:param space: Space this subnet is in.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n:param allow_proxy: Configure maas-proxy to allow requests from this subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "statistics", "method": "GET", "op": "statistics", "doc": "Returns statistics for the specified subnet, including:\n\nnum_available - the number of available IP addresses\nlargest_available - the largest number of contiguous free IP addresses\nnum_unavailable - the number of unavailable IP addresses\ntotal_addresses - the sum of the available plus unavailable addresses\nusage - the (floating point) usage percentage of this subnet\nusage_string - the (formatted unicode) usage percentage of this subnet\nranges - the specific IP ranges present in ths subnet (if specified)\n\nOptional arguments:\ninclude_ranges: if True, includes detailed information\nabout the usage of this range.\ninclude_suggestions: if True, includes the suggested gateway and\ndynamic range for this subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "reserved_ip_ranges", "method": "GET", "op": "reserved_ip_ranges", "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found."}, {"restful": false, "name": "ip_addresses", "method": "GET", "op": "ip_addresses", "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional arguments:\nwith_username: (default=True) if False, suppresses the display\nof usernames associated with each address.\nwith_node_summary: (default=True) if False, suppresses the display\nof any node associated with each address."}], "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/", "name": "SubnetHandler"}, {"params": [], "path": "/MAAS/api/2.0/dnsresourcerecords/", "doc": "Manage dnsresourcerecords.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/", "name": "DNSResourceRecordsHandler"}, {"params": ["osystem", "distro_series"], "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "doc": "Manage a license key.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete license key."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read license key."}], "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "name": "LicenseKeyHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "doc": "Manage a specific RAID device on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "name": "RaidHandler"}, {"params": [], "path": "/MAAS/api/2.0/boot-resources/", "doc": "Manage the boot resources.", "actions": [{"restful": false, "name": "import", "method": "POST", "op": "import", "doc": "Import the boot resources."}, {"restful": false, "name": "stop_import", "method": "POST", "op": "stop_import", "doc": "Stop import of boot resources."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload."}, {"restful": false, "name": "is_importing", "method": "GET", "op": "is_importing", "doc": "Return import status."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all"}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/", "name": "BootResourcesHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "doc": "Manage bcache device on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "name": "BcacheHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/", "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "list_boot_images", "method": "GET", "op": "list_boot_images", "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "power_on", "method": "POST", "op": "power_on", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "import_boot_images", "method": "POST", "op": "import_boot_images", "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found."}, {"restful": false, "name": "query_power_state", "method": "GET", "op": "query_power_state", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller."}, {"restful": false, "name": "power_off", "method": "POST", "op": "power_off", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node."}], "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/", "name": "RackControllerHandler"}, {"params": ["username"], "path": "/MAAS/api/2.0/users/{username}/", "doc": "Manage a user account.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Deletes a user"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": null}], "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/", "name": "UserHandler"}, {"params": [], "path": "/MAAS/api/2.0/subnets/", "doc": "Manage subnets.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a subnet.\n\n:param name: Name of the subnet.\n:param description: Description of the subnet.\n:param fabric: Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to or defaults to the default fabric.\n:param vlan: VLAN this subnet belongs to. Defaults to the default\n VLAN for the provided fabric or defaults to the default VLAN in\n the default fabric.\n:param vid: VID of the VLAN this subnet belongs to. Only used when\n vlan is not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n:param space: Space this subnet is in. Defaults to the default space.\n:param cidr: The network CIDR for this subnet.\n:param gateway_ip: The gateway IP address for this subnet.\n:param rdns_mode: How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means\n no reverse zone is created; Enabled means generate the reverse\n zone; RFC2317 extends Enabled to create the necessary parent zone\n with the appropriate CNAME resource records for the network, if the\n network is small enough to require the support described in\n RFC2317.\n:param allow_proxy: Configure maas-proxy to allow requests from this\n subnet.\n:param dns_servers: Comma-seperated list of DNS servers for this\n subnet."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all subnets."}], "uri": "http://localhost:5240/MAAS/api/2.0/subnets/", "name": "SubnetsHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/dnsresources/{id}/", "doc": "Manage dnsresource.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/", "name": "DNSResourceHandler"}, {"params": ["discovery_id"], "path": "/MAAS/api/2.0/discovery/{discovery_id}/", "doc": "Read or delete an observed discovery.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": null}], "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/", "name": "DiscoveryHandler"}, {"params": [], "path": "/MAAS/api/2.0/account/prefs/sslkeys/", "doc": "Operations on multiple keys.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\"."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all keys belonging to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/", "name": "SSLKeysHandler"}, {"params": [], "path": "/MAAS/api/2.0/license-keys/", "doc": "Manage the license keys.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List license keys."}], "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/", "name": "LicenseKeysHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "name": "SSLKeyHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/", "doc": "Manage bcache devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/", "name": "BcachesHandler"}, {"params": [], "path": "/MAAS/api/2.0/rackcontrollers/", "doc": "Manage the collection of all rack controllers in MAAS.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "describe_power_types", "method": "GET", "op": "describe_power_types", "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": false, "name": "import_boot_images", "method": "POST", "op": "import_boot_images", "doc": "Import the boot images on all rack controllers."}], "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/", "name": "RackControllersHandler"}, {"params": [], "path": "/MAAS/api/2.0/maas/", "doc": "Manage the MAAS server.", "actions": [{"restful": false, "name": "set_config", "method": "POST", "op": "set_config", "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)"}, {"restful": false, "name": "get_config", "method": "GET", "op": "get_config", "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)"}], "uri": "http://localhost:5240/MAAS/api/2.0/maas/", "name": "MaasHandler"}, {"params": [], "path": "/MAAS/api/2.0/discovery/", "doc": "Query observed discoveries.", "actions": [{"restful": false, "name": "scan", "method": "POST", "op": "scan", "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU."}, {"restful": false, "name": "by_unknown_ip", "method": "GET", "op": "by_unknown_ip", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": false, "name": "by_unknown_ip_and_mac", "method": "GET", "op": "by_unknown_ip_and_mac", "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}, {"restful": false, "name": "clear", "method": "POST", "op": "clear", "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data."}, {"restful": false, "name": "by_unknown_mac", "method": "GET", "op": "by_unknown_mac", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first)."}], "uri": "http://localhost:5240/MAAS/api/2.0/discovery/", "name": "DiscoveriesHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/ipranges/{id}/", "doc": "Manage IP range.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read IP range.\n\nReturns 404 if the IP range is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/", "name": "IPRangeHandler"}, {"params": [], "path": "/MAAS/api/2.0/dnsresources/", "doc": "Manage dnsresources.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/", "name": "DNSResourcesHandler"}, {"params": ["boot_source_id"], "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "doc": "Manage the collection of boot source selections.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List boot source selections.\n\nGet a listing of a boot source's selections."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "name": "BootSourceSelectionsHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "doc": "Manage bcache cache set on a machine.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "name": "BcacheCacheSetHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/", "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/", "name": "RegionControllerHandler"}, {"params": [], "path": "/MAAS/api/2.0/events/", "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.", "actions": [{"restful": false, "name": "query", "method": "GET", "op": "query", "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events."}], "uri": "http://localhost:5240/MAAS/api/2.0/events/", "name": "EventsHandler"}, {"params": [], "path": "/MAAS/api/2.0/ipranges/", "doc": "Manage IP ranges.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all IP ranges."}], "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/", "name": "IPRangesHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/domains/{id}/", "doc": "Manage domain.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read domain.\n\nReturns 404 if the domain is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/", "name": "DomainHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "doc": "Manage bcache cache sets on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "name": "BcacheCacheSetsHandler"}, {"params": [], "path": "/MAAS/api/2.0/regioncontrollers/", "doc": "Manage the collection of all region controllers in MAAS.", "actions": [{"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/", "name": "RegionControllersHandler"}, {"params": [], "path": "/MAAS/api/2.0/files/", "doc": "Manage the collection of all the files in this MAAS.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string"}, {"restful": false, "name": "get_by_key", "method": "GET", "op": "get_by_key", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode"}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied"}, {"restful": false, "name": "get", "method": "GET", "op": "get", "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/", "name": "FilesHandler"}, {"params": [], "path": "/MAAS/api/2.0/installation-results/", "doc": "Read the collection of NodeResult in the MAAS.", "actions": [{"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable"}], "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/", "name": "NodeResultsHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/static-routes/{id}/", "doc": "Manage static route.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete static route.\n\nReturns 404 if the static route is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read static route.\n\nReturns 404 if the static route is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/", "name": "StaticRouteHandler"}, {"params": [], "path": "/MAAS/api/2.0/domains/", "doc": "Manage domains.", "actions": [{"restful": false, "name": "set_serial", "method": "POST", "op": "set_serial", "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all domains."}], "uri": "http://localhost:5240/MAAS/api/2.0/domains/", "name": "DomainsHandler"}, {"params": [], "path": "/MAAS/api/2.0/tags/", "doc": "Manage the collection of all the Tags in this MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Tags.\n\nGet a listing of all tags that are currently defined."}], "uri": "http://localhost:5240/MAAS/api/2.0/tags/", "name": "TagsHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "doc": "Manage a node's or device's interface.", "actions": [{"restful": false, "name": "add_tag", "method": "POST", "op": "add_tag", "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface."}, {"restful": false, "name": "unlink_subnet", "method": "POST", "op": "unlink_subnet", "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond-mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond-miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond-downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond-updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond-lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond-xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "set_default_gateway", "method": "POST", "op": "set_default_gateway", "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "disconnect", "method": "POST", "op": "disconnect", "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "remove_tag", "method": "POST", "op": "remove_tag", "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found."}, {"restful": false, "name": "link_subnet", "method": "POST", "op": "link_subnet", "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "name": "InterfaceHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/devices/{system_id}/", "doc": "Manage an individual device.\n\nThe device is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device."}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "op": "restore_networking_configuration", "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device."}, {"restful": false, "name": "set_owner_data", "method": "POST", "op": "set_owner_data", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "op": "restore_default_configuration", "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device."}], "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/", "name": "DeviceHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "doc": "Manage block devices on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "name": "BlockDevicesHandler"}, {"params": ["filename"], "path": "/MAAS/api/2.0/files/{filename}/", "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a FileStorage object."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded."}], "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/", "name": "FileHandler"}, {"params": [], "path": "/MAAS/api/2.0/static-routes/", "doc": "Manage static routes.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all static routes."}], "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/", "name": "StaticRoutesHandler"}, {"params": [], "path": "/MAAS/api/2.0/devices/", "doc": "Manage the collection of all the devices in the MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode"}, {"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/devices/", "name": "DevicesHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/fabrics/{id}/", "doc": "Manage fabric.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read fabric.\n\nReturns 404 if the fabric is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/", "name": "FabricHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "doc": "Manage a block device on a machine.", "actions": [{"restful": false, "name": "add_tag", "method": "POST", "op": "add_tag", "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "format", "method": "POST", "op": "format", "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "unmount", "method": "POST", "op": "unmount", "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": false, "name": "unformat", "method": "POST", "op": "unformat", "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found."}, {"restful": false, "name": "remove_tag", "method": "POST", "op": "remove_tag", "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "mount", "method": "POST", "op": "mount", "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated."}, {"restful": false, "name": "set_boot_disk", "method": "POST", "op": "set_boot_disk", "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "name": "BlockDeviceHandler"}, {"params": [], "path": "/MAAS/api/2.0/ipaddresses/", "doc": "Manage IP addresses allocated by MAAS.", "actions": [{"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found."}, {"restful": false, "name": "reserve", "method": "POST", "op": "reserve", "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/", "name": "IPAddressesHandler"}, {"params": [], "path": "/MAAS/api/2.0/commissioning-scripts/", "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List commissioning scripts."}], "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/", "name": "CommissioningScriptsHandler"}, {"params": ["name"], "path": "/MAAS/api/2.0/commissioning-scripts/{name}", "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a commissioning script."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a commissioning script."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a commissioning script."}], "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}", "name": "CommissioningScriptHandler"}, {"params": [], "path": "/MAAS/api/2.0/fabrics/", "doc": "Manage fabrics.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all fabrics."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/", "name": "FabricsHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/dhcp-snippets/{id}/", "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": false, "name": "revert", "method": "POST", "op": "revert", "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/", "name": "DHCPSnippetHandler"}, {"params": ["system_id", "device_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "doc": "Manage partition on a block device.", "actions": [{"restful": false, "name": "format", "method": "POST", "op": "format", "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": false, "name": "unformat", "method": "POST", "op": "unformat", "doc": "Unformat a partition."}, {"restful": false, "name": "unmount", "method": "POST", "op": "unmount", "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found."}, {"restful": false, "name": "mount", "method": "POST", "op": "mount", "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "name": "PartitionHandler"}, {"params": ["name"], "path": "/MAAS/api/2.0/networks/{name}/", "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.", "actions": [{"restful": false, "name": "list_connected_macs", "method": "GET", "op": "list_connected_macs", "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": false, "name": "connect_macs", "method": "POST", "op": "connect_macs", "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": false, "name": "disconnect_macs", "method": "POST", "op": "disconnect_macs", "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read network definition."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators."}], "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/", "name": "NetworkHandler"}, {"params": ["name"], "path": "/MAAS/api/2.0/tags/{name}/", "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.", "actions": [{"restful": false, "name": "update_nodes", "method": "POST", "op": "update_nodes", "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "machines", "method": "GET", "op": "machines", "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "region_controllers", "method": "GET", "op": "region_controllers", "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "rack_controllers", "method": "GET", "op": "rack_controllers", "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "rebuild", "method": "POST", "op": "rebuild", "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "devices", "method": "GET", "op": "devices", "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found."}, {"restful": false, "name": "nodes", "method": "GET", "op": "nodes", "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted."}], "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/", "name": "TagHandler"}, {"params": [], "path": "/MAAS/api/2.0/account/prefs/sshkeys/", "doc": "Manage the collection of all the SSH keys in this MAAS.", "actions": [{"restful": false, "name": "import", "method": "POST", "op": "import", "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format."}, {"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\"."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all keys belonging to the requesting user."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/", "name": "SSHKeysHandler"}, {"params": [], "path": "/MAAS/api/2.0/zones/", "doc": "Manage physical zones.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List zones.\n\nGet a listing of all the physical zones."}], "uri": "http://localhost:5240/MAAS/api/2.0/zones/", "name": "ZonesHandler"}, {"params": ["name"], "path": "/MAAS/api/2.0/zones/{name}/", "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/", "name": "ZoneHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/fannetworks/{id}/", "doc": "Manage Fan Network.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/", "name": "FanNetworkHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "name": "SSHKeyHandler"}, {"params": [], "path": "/MAAS/api/2.0/fannetworks/", "doc": "Manage Fan Networks.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all fannetworks."}], "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/", "name": "FanNetworksHandler"}, {"params": [], "path": "/MAAS/api/2.0/dhcp-snippets/", "doc": "Manage the collection of all DHCP snippets in MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all DHCP snippets."}], "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/", "name": "DHCPSnippetsHandler"}, {"params": ["system_id", "device_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "doc": "Manage partitions on a block device.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "name": "PartitionsHandler"}, {"params": [], "path": "/MAAS/api/2.0/networks/", "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common."}], "uri": "http://localhost:5240/MAAS/api/2.0/networks/", "name": "NetworksHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/boot-sources/{id}/", "doc": "Manage a boot source.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific boot source."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot source."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/", "name": "BootSourceHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/", "doc": "Manage interfaces on a node.", "actions": [{"restful": false, "name": "create_physical", "method": "POST", "op": "create_physical", "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_bridge", "method": "POST", "op": "create_bridge", "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_vlan", "method": "POST", "op": "create_vlan", "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "create_bond", "method": "POST", "op": "create_bond", "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/", "name": "InterfacesHandler"}, {"params": ["fabric_id", "vid"], "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "doc": "Manage VLAN on a fabric.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:Param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n\nReturns 404 if the fabric or VLAN is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "name": "VlanHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/package-repositories/{id}/", "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/", "name": "PackageRepositoryHandler"}, {"params": ["system_id", "id"], "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "doc": "Manage volume group on a machine.", "actions": [{"restful": false, "name": "create_logical_volume", "method": "POST", "op": "create_logical_volume", "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": false, "name": "delete_logical_volume", "method": "POST", "op": "delete_logical_volume", "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "name": "VolumeGroupHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/", "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/", "name": "NodeHandler"}, {"params": [], "path": "/MAAS/api/2.0/users/", "doc": "Manage the user accounts of this MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing."}, {"restful": false, "name": "whoami", "method": "GET", "op": "whoami", "doc": "Returns the currently logged in user."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List users."}], "uri": "http://localhost:5240/MAAS/api/2.0/users/", "name": "UsersHandler"}, {"params": ["fabric_id"], "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "doc": "Manage VLANs on a fabric.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "name": "VlansHandler"}, {"params": [], "path": "/MAAS/api/2.0/boot-sources/", "doc": "Manage the collection of boot sources.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List boot sources.\n\nGet a listing of boot sources."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/", "name": "BootSourcesHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/spaces/{id}/", "doc": "Manage space.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete space.\n\nReturns 404 if the space is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read space.\n\nReturns 404 if the space is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/", "name": "SpaceHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/boot-resources/{id}/", "doc": "Manage a boot resource.", "actions": [{"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete boot resource."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot resource."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/", "name": "BootResourceHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "doc": "Manage volume groups on a machine.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "name": "VolumeGroupsHandler"}, {"params": [], "path": "/MAAS/api/2.0/nodes/", "doc": "Manage the collection of all the nodes in the MAAS.", "actions": [{"restful": false, "name": "set_zone", "method": "POST", "op": "set_zone", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode"}], "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "name": "NodesHandler"}, {"params": [], "path": "/MAAS/api/2.0/account/", "doc": "Manage the current logged-in user.", "actions": [{"restful": false, "name": "delete_authorisation_token", "method": "POST", "op": "delete_authorisation_token", "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode"}, {"restful": false, "name": "create_authorisation_token", "method": "POST", "op": "create_authorisation_token", "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)"}, {"restful": false, "name": "update_token_name", "method": "POST", "op": "update_token_name", "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode"}, {"restful": false, "name": "list_authorisation_tokens", "method": "GET", "op": "list_authorisation_tokens", "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token."}], "uri": "http://localhost:5240/MAAS/api/2.0/account/", "name": "AccountHandler"}, {"params": [], "path": "/MAAS/api/2.0/package-repositories/", "doc": "Manage the collection of all Package Repositories in MAAS.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean"}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all Package Repositories."}], "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/", "name": "PackageRepositoriesHandler"}, {"params": ["boot_source_id", "id"], "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "doc": "Manage a boot source selection.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific boot source selection.\n\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific boot source."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a boot source selection."}], "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "name": "BootSourceSelectionHandler"}, {"params": ["id"], "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/", "doc": "Manage dnsresourcerecord.", "actions": [{"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found."}], "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/", "name": "DNSResourceRecordHandler"}, {"params": [], "path": "/MAAS/api/2.0/spaces/", "doc": "Manage spaces.", "actions": [{"restful": true, "name": "create", "method": "POST", "op": null, "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "List all spaces."}], "uri": "http://localhost:5240/MAAS/api/2.0/spaces/", "name": "SpacesHandler"}, {"params": ["system_id"], "path": "/MAAS/api/2.0/machines/{system_id}/", "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.", "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "op": "power_parameters", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "mark_broken", "method": "POST", "op": "mark_broken", "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken."}, {"restful": false, "name": "unmount_special", "method": "POST", "op": "unmount_special", "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition."}, {"restful": false, "name": "commission", "method": "POST", "op": "commission", "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found."}, {"restful": false, "name": "get_curtin_config", "method": "GET", "op": "get_curtin_config", "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration."}, {"restful": false, "name": "deploy", "method": "POST", "op": "deploy", "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "clear_default_gateways", "method": "POST", "op": "clear_default_gateways", "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways."}, {"restful": false, "name": "query_power_state", "method": "GET", "op": "query_power_state", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state."}, {"restful": false, "name": "mark_fixed", "method": "POST", "op": "mark_fixed", "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed."}, {"restful": false, "name": "abort", "method": "POST", "op": "abort", "doc": "Abort a machine's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nThis currently only supports aborting of the 'Disk Erasing' operation.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation."}, {"restful": false, "name": "restore_storage_configuration", "method": "POST", "op": "restore_storage_configuration", "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": true, "name": "update", "method": "PUT", "op": null, "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine."}, {"restful": false, "name": "rescue_mode", "method": "POST", "op": "rescue_mode", "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine."}, {"restful": false, "name": "set_storage_layout", "method": "POST", "op": "set_storage_layout", "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout."}, {"restful": false, "name": "power_on", "method": "POST", "op": "power_on", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface."}, {"restful": false, "name": "release", "method": "POST", "op": "release", "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released."}, {"restful": false, "name": "exit_rescue_mode", "method": "POST", "op": "exit_rescue_mode", "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine."}, {"restful": false, "name": "power_off", "method": "POST", "op": "power_off", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node."}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "op": "restore_networking_configuration", "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": false, "name": "set_owner_data", "method": "POST", "op": "set_owner_data", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission."}, {"restful": true, "name": "delete", "method": "DELETE", "op": null, "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted."}, {"restful": false, "name": "details", "method": "GET", "op": "details", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found."}, {"restful": true, "name": "read", "method": "GET", "op": null, "doc": "Read a specific Node.\n\nReturns 404 if the node is not found."}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "op": "restore_default_configuration", "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine."}, {"restful": false, "name": "mount_special", "method": "POST", "op": "mount_special", "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition."}], "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/", "name": "MachineHandler"}], "doc": "MAAS API", "hash": "503f93ea00f40fb77070f447466a1a557bdfd409"}
\ No newline at end of file
diff --git a/maas/client/bones/testing/api22.json b/maas/client/bones/testing/api22.json
new file mode 100644
index 00000000..96170bc5
--- /dev/null
+++ b/maas/client/bones/testing/api22.json
@@ -0,0 +1,3445 @@
+{
+ "doc": "MAAS API",
+ "hash": "68ed13febb9867668ba6012ddb15647199fb2a68",
+ "resources": [
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)",
+ "method": "POST",
+ "name": "create_authorisation_token",
+ "op": "create_authorisation_token",
+ "restful": false
+ },
+ {
+ "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode",
+ "method": "POST",
+ "name": "delete_authorisation_token",
+ "op": "delete_authorisation_token",
+ "restful": false
+ },
+ {
+ "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token.",
+ "method": "GET",
+ "name": "list_authorisation_tokens",
+ "op": "list_authorisation_tokens",
+ "restful": false
+ },
+ {
+ "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode",
+ "method": "POST",
+ "name": "update_token_name",
+ "op": "update_token_name",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the current logged-in user.",
+ "name": "AccountHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/"
+ },
+ "name": "AccountHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache set on a machine.",
+ "name": "BcacheCacheSetHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/"
+ },
+ "name": "BcacheCacheSetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache cache sets on a machine.",
+ "name": "BcacheCacheSetsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/"
+ },
+ "name": "BcacheCacheSetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache device on a machine.",
+ "name": "BcacheHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/"
+ },
+ "name": "BcacheHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage bcache devices on a machine.",
+ "name": "BcachesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/"
+ },
+ "name": "BcachesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "add_tag",
+ "op": "add_tag",
+ "restful": false
+ },
+ {
+ "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "remove_tag",
+ "op": "remove_tag",
+ "restful": false
+ },
+ {
+ "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "set_boot_disk",
+ "op": "set_boot_disk",
+ "restful": false
+ },
+ {
+ "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ },
+ {
+ "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a block device on a machine.",
+ "name": "BlockDeviceHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/"
+ },
+ "name": "BlockDeviceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage block devices on a machine.",
+ "name": "BlockDevicesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/"
+ },
+ "name": "BlockDevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete boot resource.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot resource.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot resource.",
+ "name": "BootResourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-resources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/"
+ },
+ "name": "BootResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Import the boot resources.",
+ "method": "POST",
+ "name": "import",
+ "op": "import",
+ "restful": false
+ },
+ {
+ "doc": "Return import status.",
+ "method": "GET",
+ "name": "is_importing",
+ "op": "is_importing",
+ "restful": false
+ },
+ {
+ "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Stop import of boot resources.",
+ "method": "POST",
+ "name": "stop_import",
+ "op": "stop_import",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the boot resources.",
+ "name": "BootResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-resources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/"
+ },
+ "name": "BootResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source.",
+ "name": "BootSourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/"
+ },
+ "name": "BootSourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific boot source.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a boot source selection.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a boot source selection.",
+ "name": "BootSourceSelectionHandler",
+ "params": [
+ "boot_source_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/"
+ },
+ "name": "BootSourceSelectionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot source selections.\n\nGet a listing of a boot source's selections.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot source selections.",
+ "name": "BootSourceSelectionsHandler",
+ "params": [
+ "boot_source_id"
+ ],
+ "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/"
+ },
+ "name": "BootSourceSelectionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List boot sources.\n\nGet a listing of boot sources.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of boot sources.",
+ "name": "BootSourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/boot-sources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/"
+ },
+ "name": "BootSourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a commissioning script.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read a commissioning script.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a commissioning script.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/commissioning-scripts/{name}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}"
+ },
+ "name": "CommissioningScriptHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List commissioning scripts.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.",
+ "name": "CommissioningScriptsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/commissioning-scripts/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/"
+ },
+ "name": "CommissioningScriptsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "POST",
+ "name": "revert",
+ "op": "revert",
+ "restful": false
+ },
+ {
+ "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.",
+ "name": "DHCPSnippetHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dhcp-snippets/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/"
+ },
+ "name": "DHCPSnippetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all DHCP snippets.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all DHCP snippets in MAAS.",
+ "name": "DHCPSnippetsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dhcp-snippets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/"
+ },
+ "name": "DHCPSnippetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresource.",
+ "name": "DNSResourceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresources/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/"
+ },
+ "name": "DNSResourceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecord.",
+ "name": "DNSResourceRecordHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/"
+ },
+ "name": "DNSResourceRecordHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresourcerecords.",
+ "name": "DNSResourceRecordsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresourcerecords/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/"
+ },
+ "name": "DNSResourceRecordsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage dnsresources.",
+ "name": "DNSResourcesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/dnsresources/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/"
+ },
+ "name": "DNSResourcesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device.",
+ "method": "POST",
+ "name": "restore_default_configuration",
+ "op": "restore_default_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device.",
+ "method": "POST",
+ "name": "restore_networking_configuration",
+ "op": "restore_networking_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.",
+ "method": "POST",
+ "name": "set_owner_data",
+ "op": "set_owner_data",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual device.\n\nThe device is identified by its system_id.",
+ "name": "DeviceHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/devices/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/"
+ },
+ "name": "DeviceHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the devices in the MAAS.",
+ "name": "DevicesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/devices/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/devices/"
+ },
+ "name": "DevicesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_ip",
+ "op": "by_unknown_ip",
+ "restful": false
+ },
+ {
+ "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_ip_and_mac",
+ "op": "by_unknown_ip_and_mac",
+ "restful": false
+ },
+ {
+ "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "by_unknown_mac",
+ "op": "by_unknown_mac",
+ "restful": false
+ },
+ {
+ "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data.",
+ "method": "POST",
+ "name": "clear",
+ "op": "clear",
+ "restful": false
+ },
+ {
+ "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU.",
+ "method": "POST",
+ "name": "scan",
+ "op": "scan",
+ "restful": false
+ }
+ ],
+ "doc": "Query observed discoveries.",
+ "name": "DiscoveriesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/discovery/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/discovery/"
+ },
+ "name": "DiscoveriesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Read or delete an observed discovery.",
+ "name": "DiscoveryHandler",
+ "params": [
+ "discovery_id"
+ ],
+ "path": "/MAAS/api/2.0/discovery/{discovery_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/"
+ },
+ "name": "DiscoveryHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read domain.\n\nReturns 404 if the domain is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage domain.",
+ "name": "DomainHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/domains/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/"
+ },
+ "name": "DomainHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all domains.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next.",
+ "method": "POST",
+ "name": "set_serial",
+ "op": "set_serial",
+ "restful": false
+ }
+ ],
+ "doc": "Manage domains.",
+ "name": "DomainsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/domains/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/domains/"
+ },
+ "name": "DomainsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events.",
+ "method": "GET",
+ "name": "query",
+ "op": "query",
+ "restful": false
+ }
+ ],
+ "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.",
+ "name": "EventsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/events/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/events/"
+ },
+ "name": "EventsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabric.",
+ "name": "FabricHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/"
+ },
+ "name": "FabricHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fabrics.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage fabrics.",
+ "name": "FabricsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fabrics/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/"
+ },
+ "name": "FabricsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Network.",
+ "name": "FanNetworkHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/fannetworks/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/"
+ },
+ "name": "FanNetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all fannetworks.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage Fan Networks.",
+ "name": "FanNetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/fannetworks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/"
+ },
+ "name": "FanNetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a FileStorage object.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.",
+ "name": "FileHandler",
+ "params": [
+ "filename"
+ ],
+ "path": "/MAAS/api/2.0/files/{filename}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/"
+ },
+ "name": "FileHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.",
+ "name": "AnonFilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get",
+ "op": "get",
+ "restful": false
+ },
+ {
+ "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.",
+ "method": "GET",
+ "name": "get_by_key",
+ "op": "get_by_key",
+ "restful": false
+ },
+ {
+ "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the files in this MAAS.",
+ "name": "FilesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/files/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/files/"
+ },
+ "name": "FilesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available.",
+ "method": "POST",
+ "name": "reserve",
+ "op": "reserve",
+ "restful": false
+ }
+ ],
+ "doc": "Manage IP addresses allocated by MAAS.",
+ "name": "IPAddressesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/ipaddresses/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/"
+ },
+ "name": "IPAddressesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read IP range.\n\nReturns 404 if the IP range is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage IP range.",
+ "name": "IPRangeHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/ipranges/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/"
+ },
+ "name": "IPRangeHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all IP ranges.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage IP ranges.",
+ "name": "IPRangesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/ipranges/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/"
+ },
+ "name": "IPRangesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.",
+ "method": "POST",
+ "name": "add_tag",
+ "op": "add_tag",
+ "restful": false
+ },
+ {
+ "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "disconnect",
+ "op": "disconnect",
+ "restful": false
+ },
+ {
+ "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "link_subnet",
+ "op": "link_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.",
+ "method": "POST",
+ "name": "remove_tag",
+ "op": "remove_tag",
+ "restful": false
+ },
+ {
+ "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "set_default_gateway",
+ "op": "set_default_gateway",
+ "restful": false
+ },
+ {
+ "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.",
+ "method": "POST",
+ "name": "unlink_subnet",
+ "op": "unlink_subnet",
+ "restful": false
+ },
+ {
+ "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a node's or device's interface.",
+ "name": "InterfaceHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/"
+ },
+ "name": "InterfaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bond",
+ "op": "create_bond",
+ "restful": false
+ },
+ {
+ "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_bridge",
+ "op": "create_bridge",
+ "restful": false
+ },
+ {
+ "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_physical",
+ "op": "create_physical",
+ "restful": false
+ },
+ {
+ "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "create_vlan",
+ "op": "create_vlan",
+ "restful": false
+ },
+ {
+ "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage interfaces on a node.",
+ "name": "InterfacesHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/"
+ },
+ "name": "InterfacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete license key.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read license key.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a license key.",
+ "name": "LicenseKeyHandler",
+ "params": [
+ "osystem",
+ "distro_series"
+ ],
+ "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}"
+ },
+ "name": "LicenseKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List license keys.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the license keys.",
+ "name": "LicenseKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/license-keys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/"
+ },
+ "name": "LicenseKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)",
+ "method": "GET",
+ "name": "get_config",
+ "op": "get_config",
+ "restful": false
+ },
+ {
+ "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)",
+ "method": "POST",
+ "name": "set_config",
+ "op": "set_config",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the MAAS server.",
+ "name": "MaasHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/maas/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/maas/"
+ },
+ "name": "MaasHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.",
+ "method": "POST",
+ "name": "abort",
+ "op": "abort",
+ "restful": false
+ },
+ {
+ "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways.",
+ "method": "POST",
+ "name": "clear_default_gateways",
+ "op": "clear_default_gateways",
+ "restful": false
+ },
+ {
+ "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n:param commissioning_scripts: A comma seperated list of commissioning\n script names and tags to be run. By default all custom\n commissioning scripts are run. Builtin commissioning scripts always\n run.\n:type commissioning_scripts: string\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run. Set to 'none' to disable running tests.\n:type testing_scripts: string\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found.",
+ "method": "POST",
+ "name": "commission",
+ "op": "commission",
+ "restful": false
+ },
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "deploy",
+ "op": "deploy",
+ "restful": false
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine.",
+ "method": "POST",
+ "name": "exit_rescue_mode",
+ "op": "exit_rescue_mode",
+ "restful": false
+ },
+ {
+ "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration.",
+ "method": "GET",
+ "name": "get_curtin_config",
+ "op": "get_curtin_config",
+ "restful": false
+ },
+ {
+ "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.",
+ "method": "POST",
+ "name": "mark_broken",
+ "op": "mark_broken",
+ "restful": false
+ },
+ {
+ "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed.",
+ "method": "POST",
+ "name": "mark_fixed",
+ "op": "mark_fixed",
+ "restful": false
+ },
+ {
+ "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition.",
+ "method": "POST",
+ "name": "mount_special",
+ "op": "mount_special",
+ "restful": false
+ },
+ {
+ "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.",
+ "method": "POST",
+ "name": "power_off",
+ "op": "power_off",
+ "restful": false
+ },
+ {
+ "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "power_on",
+ "op": "power_on",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.",
+ "method": "GET",
+ "name": "query_power_state",
+ "op": "query_power_state",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine.",
+ "method": "POST",
+ "name": "rescue_mode",
+ "op": "rescue_mode",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_default_configuration",
+ "op": "restore_default_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_networking_configuration",
+ "op": "restore_networking_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.",
+ "method": "POST",
+ "name": "restore_storage_configuration",
+ "op": "restore_storage_configuration",
+ "restful": false
+ },
+ {
+ "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.",
+ "method": "POST",
+ "name": "set_owner_data",
+ "op": "set_owner_data",
+ "restful": false
+ },
+ {
+ "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout.",
+ "method": "POST",
+ "name": "set_storage_layout",
+ "op": "set_storage_layout",
+ "restful": false
+ },
+ {
+ "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "test",
+ "op": "test",
+ "restful": false
+ },
+ {
+ "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition.",
+ "method": "POST",
+ "name": "unmount_special",
+ "op": "unmount_special",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.",
+ "name": "MachineHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/machines/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/"
+ },
+ "name": "MachineHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Machines.",
+ "name": "AnonMachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "accept",
+ "op": "accept",
+ "restful": false
+ },
+ {
+ "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.",
+ "method": "POST",
+ "name": "accept_all",
+ "op": "accept_all",
+ "restful": false
+ },
+ {
+ "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed.",
+ "method": "POST",
+ "name": "add_chassis",
+ "op": "add_chassis",
+ "restful": false
+ },
+ {
+ "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound.",
+ "method": "POST",
+ "name": "allocate",
+ "op": "allocate",
+ "restful": false
+ },
+ {
+ "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Fetch Machines that were allocated to the User/oauth token.",
+ "method": "GET",
+ "name": "list_allocated",
+ "op": "list_allocated",
+ "restful": false
+ },
+ {
+ "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state.",
+ "method": "POST",
+ "name": "release",
+ "op": "release",
+ "restful": false
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the machines in the MAAS.",
+ "name": "MachinesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/machines/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/machines/"
+ },
+ "name": "MachinesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "connect_macs",
+ "op": "connect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.",
+ "method": "POST",
+ "name": "disconnect_macs",
+ "op": "disconnect_macs",
+ "restful": false
+ },
+ {
+ "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned.",
+ "method": "GET",
+ "name": "list_connected_macs",
+ "op": "list_connected_macs",
+ "restful": false
+ },
+ {
+ "doc": "Read network definition.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.",
+ "name": "NetworkHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/networks/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/"
+ },
+ "name": "NetworkHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.",
+ "name": "NetworksHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/networks/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/networks/"
+ },
+ "name": "NetworksHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.",
+ "name": "NodeHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/"
+ },
+ "name": "NodeHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Read the collection of NodeResult in the MAAS.",
+ "name": "NodeResultsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/installation-results/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/"
+ },
+ "name": "NodeResultsHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all the nodes in the MAAS.",
+ "name": "NodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "name": "NodesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific notification.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Dismiss a specific notification.\n\nReturns HTTP 403 FORBIDDEN if this notification is not relevant\n(targeted) to the invoking user.\n\nIt is safe to call multiple times for the same notification.",
+ "method": "POST",
+ "name": "dismiss",
+ "op": "dismiss",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific notification.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific notification.\n\nSee `NotificationsHandler.create` for field information.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual notification.",
+ "name": "NotificationHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/notifications/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/notifications/{id}/"
+ },
+ "name": "NotificationHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a notification.\n\nThis is available to admins *only*.\n\n:param message: The message for this notification. May contain basic\n HTML; this will be sanitised before display.\n:param context: Optional JSON context. The root object *must* be an\n object (i.e. a mapping). The values herein can be referenced by\n `message` with Python's \"format\" (not %) codes.\n:param category: Optional category. Choose from: error, warning,\n success, or info. Defaults to info.\n\n:param ident: Optional unique identifier for this notification.\n:param user: Optional user ID this notification is intended for. By\n default it will not be targeted to any individual user.\n:param users: Optional boolean, true to notify all users, defaults to\n false, i.e. not targeted to all users.\n:param admins: Optional boolean, true to notify all admins, defaults to\n false, i.e. not targeted to all admins.\n\nNote: if neither user nor users nor admins is set, the notification\nwill not be seen by anyone.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List notifications relevant to the invoking user.\n\nNotifications that have been dismissed are *not* returned.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the notifications in MAAS.",
+ "name": "NotificationsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/notifications/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/notifications/"
+ },
+ "name": "NotificationsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all Package Repositories.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all Package Repositories in MAAS.",
+ "name": "PackageRepositoriesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/package-repositories/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/"
+ },
+ "name": "PackageRepositoriesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.",
+ "name": "PackageRepositoryHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/package-repositories/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/"
+ },
+ "name": "PackageRepositoryHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "format",
+ "op": "format",
+ "restful": false
+ },
+ {
+ "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "mount",
+ "op": "mount",
+ "restful": false
+ },
+ {
+ "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Unformat a partition.",
+ "method": "POST",
+ "name": "unformat",
+ "op": "unformat",
+ "restful": false
+ },
+ {
+ "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found.",
+ "method": "POST",
+ "name": "unmount",
+ "op": "unmount",
+ "restful": false
+ }
+ ],
+ "doc": "Manage partition on a block device.",
+ "name": "PartitionHandler",
+ "params": [
+ "system_id",
+ "device_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}"
+ },
+ "name": "PartitionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage partitions on a block device.",
+ "name": "PartitionsHandler",
+ "params": [
+ "system_id",
+ "device_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/"
+ },
+ "name": "PartitionsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Compose a machine from Pod.\n\nAll fields below are optional:\n\n:param cores: Minimum number of CPU cores.\n:param memory: Minimum amount of memory (MiB).\n:param cpu_speed: Minimum amount of CPU speed (MHz).\n:param architecture: Architecture for the machine. Must be an\n architecture that the pod supports.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to compose machine.",
+ "method": "POST",
+ "name": "compose",
+ "op": "compose",
+ "restful": false
+ },
+ {
+ "doc": "Delete a specific Pod.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to delete the pod.\nReturns 204 if the pod is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain pod parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the pod parameters, if any, configured for a\npod. For some types of pod this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the pod is not found.",
+ "method": "GET",
+ "name": "parameters",
+ "op": "parameters",
+ "restful": false
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Refresh a specific Pod.\n\nPerforms pod discovery and updates all discovered information and\ndiscovered machines.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to refresh the pod.",
+ "method": "POST",
+ "name": "refresh",
+ "op": "refresh",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Pod.\n\n:param name: Name for the pod (optional).\n\nNote: 'type' cannot be updated on a Pod. The Pod must be deleted and\nre-added to change the type.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to update the pod.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual pod.\n\nThe pod is identified by its id.",
+ "name": "PodHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/pods/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/pods/{id}/"
+ },
+ "name": "PodHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a Pod.\n\n:param type: Type of pod to create.\n:param name: Name for the pod (optional).\n\nReturns 503 if the pod could not be discovered.\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to create a pod.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List pods.\n\nGet a listing of all the pods.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the pod in the MAAS.",
+ "name": "PodsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/pods/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/pods/"
+ },
+ "name": "PodsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.",
+ "method": "POST",
+ "name": "abort",
+ "op": "abort",
+ "restful": false
+ },
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found.",
+ "method": "POST",
+ "name": "import_boot_images",
+ "op": "import_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found.",
+ "method": "GET",
+ "name": "list_boot_images",
+ "op": "list_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.",
+ "method": "POST",
+ "name": "power_off",
+ "op": "power_off",
+ "restful": false
+ },
+ {
+ "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.",
+ "method": "POST",
+ "name": "power_on",
+ "op": "power_on",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.",
+ "method": "GET",
+ "name": "query_power_state",
+ "op": "query_power_state",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.",
+ "method": "POST",
+ "name": "test",
+ "op": "test",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.",
+ "name": "RackControllerHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/"
+ },
+ "name": "RackControllerHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format.",
+ "method": "GET",
+ "name": "describe_power_types",
+ "op": "describe_power_types",
+ "restful": false
+ },
+ {
+ "doc": "Import the boot images on all rack controllers.",
+ "method": "POST",
+ "name": "import_boot_images",
+ "op": "import_boot_images",
+ "restful": false
+ },
+ {
+ "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all rack controllers in MAAS.",
+ "name": "RackControllersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/rackcontrollers/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/"
+ },
+ "name": "RackControllersHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a specific RAID device on a machine.",
+ "name": "RaidHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/"
+ },
+ "name": "RaidHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage all RAID devices on a machine.",
+ "name": "RaidsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/raids/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/"
+ },
+ "name": "RaidsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "details",
+ "op": "details",
+ "restful": false
+ },
+ {
+ "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "power_parameters",
+ "op": "power_parameters",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.",
+ "name": "RegionControllerHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/"
+ },
+ "name": "RegionControllerHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "GET",
+ "name": "is_registered",
+ "op": "is_registered",
+ "restful": false
+ }
+ ],
+ "doc": "Anonymous access to Nodes.",
+ "name": "AnonNodesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/nodes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/"
+ },
+ "auth": {
+ "actions": [
+ {
+ "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.",
+ "method": "POST",
+ "name": "set_zone",
+ "op": "set_zone",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the collection of all region controllers in MAAS.",
+ "name": "RegionControllersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/regioncontrollers/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/"
+ },
+ "name": "RegionControllersHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.",
+ "name": "SSHKeyHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/"
+ },
+ "name": "SSHKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format.",
+ "method": "POST",
+ "name": "import",
+ "op": "import",
+ "restful": false
+ },
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the SSH keys in this MAAS.",
+ "name": "SSHKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sshkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/"
+ },
+ "name": "SSHKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.",
+ "name": "SSLKeyHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/"
+ },
+ "name": "SSLKeyHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\".",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all keys belonging to the requesting user.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Operations on multiple keys.",
+ "name": "SSLKeysHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/account/prefs/sslkeys/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/"
+ },
+ "name": "SSLKeysHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete space.\n\nReturns 404 if the space is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read space.\n\nReturns 404 if the space is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage space.",
+ "name": "SpaceHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/spaces/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/"
+ },
+ "name": "SpaceHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all spaces.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage spaces.",
+ "name": "SpacesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/spaces/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/spaces/"
+ },
+ "name": "SpacesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete static route.\n\nReturns 404 if the static route is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read static route.\n\nReturns 404 if the static route is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage static route.",
+ "name": "StaticRouteHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/static-routes/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/"
+ },
+ "name": "StaticRouteHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all static routes.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage static routes.",
+ "name": "StaticRoutesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/static-routes/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/"
+ },
+ "name": "StaticRoutesHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional parameters\n-------------------\n\nwith_username\n If False, suppresses the display of usernames associated with each\n address. (Default: True)\n\nwith_node_summary\n If False, suppresses the display of any node associated with each\n address. (Default: True)",
+ "method": "GET",
+ "name": "ip_addresses",
+ "op": "ip_addresses",
+ "restful": false
+ },
+ {
+ "doc": "Read subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "reserved_ip_ranges",
+ "op": "reserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Returns statistics for the specified subnet, including:\n\nnum_available: the number of available IP addresses\nlargest_available: the largest number of contiguous free IP addresses\nnum_unavailable: the number of unavailable IP addresses\ntotal_addresses: the sum of the available plus unavailable addresses\nusage: the (floating point) usage percentage of this subnet\nusage_string: the (formatted unicode) usage percentage of this subnet\nranges: the specific IP ranges present in ths subnet (if specified)\n\nOptional parameters\n-------------------\n\ninclude_ranges\n If True, includes detailed information\n about the usage of this range.\n\ninclude_suggestions\n If True, includes the suggested gateway and dynamic range for this\n subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "statistics",
+ "op": "statistics",
+ "restful": false
+ },
+ {
+ "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found.",
+ "method": "GET",
+ "name": "unreserved_ip_ranges",
+ "op": "unreserved_ip_ranges",
+ "restful": false
+ },
+ {
+ "doc": "Update the specified subnet.\n\nPlease see the documentation for the 'create' operation for detailed\ndescriptions of each parameter.\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to.\n\nspace\n Space this subnet is in.\n\ncidr\n The network CIDR for this subnet.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n\nallow_proxy\n Configure maas-proxy to allow requests from this subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n If False, MAAS should not manage this subnet. (Default: True)\n\nReturns 404 if the subnet is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnet.",
+ "name": "SubnetHandler",
+ "params": [
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/subnets/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/"
+ },
+ "name": "SubnetHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a subnet.\n\nRequired parameters\n-------------------\n\ncidr\n The network CIDR for this subnet.\n\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to. Defaults to the default VLAN for the\n provided fabric or defaults to the default VLAN in the default fabric\n (if unspecified).\n\nfabric\n Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to, or defaults to the default fabric.\n\nvid\n VID of the VLAN this subnet belongs to. Only used when vlan is\n not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n\nspace\n Space this subnet is in. Defaults to the default space.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled\n means no reverse zone is created; Enabled means generate the\n reverse zone; RFC2317 extends Enabled to create the necessary\n parent zone with the appropriate CNAME resource records for the\n network, if the network is small enough to require the support\n described in RFC2317.\n\nallow_proxy\n Configure maas-proxy to allow requests from this\n subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n In MAAS 2.0+, all subnets are assumed to be managed by default.\n\n Only managed subnets allow DHCP to be enabled on their related\n dynamic ranges. (Thus, dynamic ranges become \"informational\n only\"; an indication that another DHCP server is currently\n handling them, or that MAAS will handle them when the subnet is\n enabled for management.)\n\n Managed subnets do not allow IP allocation by default. The\n meaning of a \"reserved\" IP range is reversed for an unmanaged\n subnet. (That is, for managed subnets, \"reserved\" means \"MAAS\n cannot allocate any IP address within this reserved block\". For\n unmanaged subnets, \"reserved\" means \"MAAS must allocate IP\n addresses only from reserved IP ranges\".",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all subnets.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage subnets.",
+ "name": "SubnetsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/subnets/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/subnets/"
+ },
+ "name": "SubnetsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "devices",
+ "op": "devices",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "machines",
+ "op": "machines",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "nodes",
+ "op": "nodes",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "rack_controllers",
+ "op": "rack_controllers",
+ "restful": false
+ },
+ {
+ "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found.",
+ "method": "POST",
+ "name": "rebuild",
+ "op": "rebuild",
+ "restful": false
+ },
+ {
+ "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found.",
+ "method": "GET",
+ "name": "region_controllers",
+ "op": "region_controllers",
+ "restful": false
+ },
+ {
+ "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition.",
+ "method": "POST",
+ "name": "update_nodes",
+ "op": "update_nodes",
+ "restful": false
+ }
+ ],
+ "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.",
+ "name": "TagHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/tags/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/"
+ },
+ "name": "TagHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List Tags.\n\nGet a listing of all tags that are currently defined.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage the collection of all the Tags in this MAAS.",
+ "name": "TagsHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/tags/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/tags/"
+ },
+ "name": "TagsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Deletes a user",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": null,
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a user account.",
+ "name": "UserHandler",
+ "params": [
+ "username"
+ ],
+ "path": "/MAAS/api/2.0/users/{username}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/"
+ },
+ "name": "UserHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List users.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Returns the currently logged in user.",
+ "method": "GET",
+ "name": "whoami",
+ "op": "whoami",
+ "restful": false
+ }
+ ],
+ "doc": "Manage the user accounts of this MAAS.",
+ "name": "UsersHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/users/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/users/"
+ },
+ "name": "UsersHandler"
+ },
+ {
+ "anon": {
+ "actions": [
+ {
+ "doc": "Version and capabilities of this MAAS instance.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }",
+ "name": "VersionHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/version/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/version/"
+ },
+ "auth": null,
+ "name": "VersionHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n:param relay_vlan: Only set when this VLAN will be using a DHCP relay\n to forward DHCP requests to another VLAN that MAAS is or will run\n the DHCP server. MAAS will not run the DHCP relay itself, it must\n be configured to proxy reqests to the primary and/or secondary\n rack controller interfaces for the VLAN specified in this field.\n:type relay_vlan: ID of VLAN\n:param space: The space this VLAN should be placed in. Passing in an\n empty string (or the string 'undefined') will cause the VLAN to be\n placed in the 'undefined' space.\n:type space: unicode\n\nReturns 404 if the fabric or VLAN is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLAN on a fabric.",
+ "name": "VlanHandler",
+ "params": [
+ "fabric_id",
+ "vid"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/"
+ },
+ "name": "VlanHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage VLANs on a fabric.",
+ "name": "VlansHandler",
+ "params": [
+ "fabric_id"
+ ],
+ "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/"
+ },
+ "name": "VlansHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create_logical_volume",
+ "op": "create_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "delete_logical_volume",
+ "op": "delete_logical_volume",
+ "restful": false
+ },
+ {
+ "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume group on a machine.",
+ "name": "VolumeGroupHandler",
+ "params": [
+ "system_id",
+ "id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/"
+ },
+ "name": "VolumeGroupHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage volume groups on a machine.",
+ "name": "VolumeGroupsHandler",
+ "params": [
+ "system_id"
+ ],
+ "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/"
+ },
+ "name": "VolumeGroupsHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted.",
+ "method": "DELETE",
+ "name": "delete",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found.",
+ "method": "PUT",
+ "name": "update",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.",
+ "name": "ZoneHandler",
+ "params": [
+ "name"
+ ],
+ "path": "/MAAS/api/2.0/zones/{name}/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/"
+ },
+ "name": "ZoneHandler"
+ },
+ {
+ "anon": null,
+ "auth": {
+ "actions": [
+ {
+ "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode",
+ "method": "POST",
+ "name": "create",
+ "op": null,
+ "restful": true
+ },
+ {
+ "doc": "List zones.\n\nGet a listing of all the physical zones.",
+ "method": "GET",
+ "name": "read",
+ "op": null,
+ "restful": true
+ }
+ ],
+ "doc": "Manage physical zones.",
+ "name": "ZonesHandler",
+ "params": [],
+ "path": "/MAAS/api/2.0/zones/",
+ "uri": "http://localhost:5240/MAAS/api/2.0/zones/"
+ },
+ "name": "ZonesHandler"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/maas/client/bones/testing/api22.raw.json b/maas/client/bones/testing/api22.raw.json
new file mode 100644
index 00000000..9669803e
--- /dev/null
+++ b/maas/client/bones/testing/api22.raw.json
@@ -0,0 +1 @@
+{"hash": "68ed13febb9867668ba6012ddb15647199fb2a68", "doc": "MAAS API", "handlers": [{"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, {"name": "VersionHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Version and capabilities of this MAAS instance.", "op": null}], "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }", "uri": "http://localhost:5240/MAAS/api/2.0/version/", "path": "/MAAS/api/2.0/version/"}, {"name": "AnonMachinesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode", "op": null}, {"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}, {"restful": false, "name": "accept", "method": "POST", "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401.", "op": "accept"}], "doc": "Anonymous access to Machines.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "path": "/MAAS/api/2.0/machines/"}, {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, {"name": "AnonFilesHandler", "params": [], "actions": [{"restful": false, "name": "get_by_key", "method": "GET", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.", "op": "get_by_key"}], "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.", "uri": "http://localhost:5240/MAAS/api/2.0/files/", "path": "/MAAS/api/2.0/files/"}, {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, {"name": "NetworksHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common.", "op": null}], "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.", "uri": "http://localhost:5240/MAAS/api/2.0/networks/", "path": "/MAAS/api/2.0/networks/"}, {"name": "TagHandler", "params": ["name"], "actions": [{"restful": false, "name": "update_nodes", "method": "POST", "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition.", "op": "update_nodes"}, {"restful": false, "name": "rebuild", "method": "POST", "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found.", "op": "rebuild"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found.", "op": null}, {"restful": false, "name": "rack_controllers", "method": "GET", "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found.", "op": "rack_controllers"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found.", "op": null}, {"restful": false, "name": "machines", "method": "GET", "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found.", "op": "machines"}, {"restful": false, "name": "devices", "method": "GET", "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found.", "op": "devices"}, {"restful": false, "name": "region_controllers", "method": "GET", "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found.", "op": "region_controllers"}, {"restful": false, "name": "nodes", "method": "GET", "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found.", "op": "nodes"}], "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.", "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/", "path": "/MAAS/api/2.0/tags/{name}/"}, {"name": "DHCPSnippetHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": false, "name": "revert", "method": "POST", "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found.", "op": "revert"}], "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/", "path": "/MAAS/api/2.0/dhcp-snippets/{id}/"}, {"name": "PartitionHandler", "params": ["system_id", "device_id", "id"], "actions": [{"restful": false, "name": "mount", "method": "POST", "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "mount"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found.", "op": null}, {"restful": false, "name": "unformat", "method": "POST", "doc": "Unformat a partition.", "op": "unformat"}, {"restful": false, "name": "format", "method": "POST", "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "format"}, {"restful": false, "name": "unmount", "method": "POST", "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "unmount"}], "doc": "Manage partition on a block device.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}"}, {"name": "StaticRouteHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read static route.\n\nReturns 404 if the static route is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete static route.\n\nReturns 404 if the static route is not found.", "op": null}], "doc": "Manage static route.", "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/", "path": "/MAAS/api/2.0/static-routes/{id}/"}, {"name": "NodeHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}], "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/"}, {"name": "PodsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a Pod.\n\n:param type: Type of pod to create.\n:param name: Name for the pod (optional).\n\nReturns 503 if the pod could not be discovered.\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to create a pod.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List pods.\n\nGet a listing of all the pods.", "op": null}], "doc": "Manage the collection of all the pod in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/pods/", "path": "/MAAS/api/2.0/pods/"}, {"name": "FabricHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read fabric.\n\nReturns 404 if the fabric is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found.", "op": null}], "doc": "Manage fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/", "path": "/MAAS/api/2.0/fabrics/{id}/"}, {"name": "TagsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List Tags.\n\nGet a listing of all tags that are currently defined.", "op": null}], "doc": "Manage the collection of all the Tags in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/tags/", "path": "/MAAS/api/2.0/tags/"}, {"name": "DHCPSnippetsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all DHCP snippets.", "op": null}], "doc": "Manage the collection of all DHCP snippets in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/", "path": "/MAAS/api/2.0/dhcp-snippets/"}, {"name": "PartitionsHandler", "params": ["system_id", "device_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found.", "op": null}], "doc": "Manage partitions on a block device.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/"}, {"name": "NodesHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all the nodes in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, {"name": "StaticRoutesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all static routes.", "op": null}], "doc": "Manage static routes.", "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/", "path": "/MAAS/api/2.0/static-routes/"}, {"name": "FabricsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all fabrics.", "op": null}], "doc": "Manage fabrics.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/", "path": "/MAAS/api/2.0/fabrics/"}, {"name": "PackageRepositoryHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found.", "op": null}], "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/", "path": "/MAAS/api/2.0/package-repositories/{id}/"}, {"name": "MachineHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_off", "method": "POST", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.", "op": "power_off"}, {"restful": false, "name": "mount_special", "method": "POST", "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition.", "op": "mount_special"}, {"restful": false, "name": "unmount_special", "method": "POST", "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition.", "op": "unmount_special"}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_default_configuration"}, {"restful": false, "name": "mark_fixed", "method": "POST", "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed.", "op": "mark_fixed"}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_networking_configuration"}, {"restful": false, "name": "mark_broken", "method": "POST", "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.", "op": "mark_broken"}, {"restful": false, "name": "commission", "method": "POST", "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n:param commissioning_scripts: A comma seperated list of commissioning\n script names and tags to be run. By default all custom\n commissioning scripts are run. Builtin commissioning scripts always\n run.\n:type commissioning_scripts: string\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run. Set to 'none' to disable running tests.\n:type testing_scripts: string\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found.", "op": "commission"}, {"restful": false, "name": "test", "method": "POST", "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.", "op": "test"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "release", "method": "POST", "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released.", "op": "release"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "restore_storage_configuration", "method": "POST", "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_storage_configuration"}, {"restful": false, "name": "set_owner_data", "method": "POST", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.", "op": "set_owner_data"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}, {"restful": false, "name": "clear_default_gateways", "method": "POST", "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways.", "op": "clear_default_gateways"}, {"restful": false, "name": "exit_rescue_mode", "method": "POST", "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine.", "op": "exit_rescue_mode"}, {"restful": false, "name": "rescue_mode", "method": "POST", "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine.", "op": "rescue_mode"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine.", "op": null}, {"restful": false, "name": "get_curtin_config", "method": "GET", "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration.", "op": "get_curtin_config"}, {"restful": false, "name": "deploy", "method": "POST", "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "deploy"}, {"restful": false, "name": "power_on", "method": "POST", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "power_on"}, {"restful": false, "name": "abort", "method": "POST", "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.", "op": "abort"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": false, "name": "set_storage_layout", "method": "POST", "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout.", "op": "set_storage_layout"}, {"restful": false, "name": "query_power_state", "method": "GET", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.", "op": "query_power_state"}], "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/", "path": "/MAAS/api/2.0/machines/{system_id}/"}, {"name": "VolumeGroupHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.", "op": null}, {"restful": false, "name": "create_logical_volume", "method": "POST", "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": "create_logical_volume"}, {"restful": false, "name": "delete_logical_volume", "method": "POST", "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": "delete_logical_volume"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage volume group on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/"}, {"name": "NotificationHandler", "params": ["id"], "actions": [{"restful": false, "name": "dismiss", "method": "POST", "doc": "Dismiss a specific notification.\n\nReturns HTTP 403 FORBIDDEN if this notification is not relevant\n(targeted) to the invoking user.\n\nIt is safe to call multiple times for the same notification.", "op": "dismiss"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific notification.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific notification.\n\nSee `NotificationsHandler.create` for field information.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific notification.", "op": null}], "doc": "Manage an individual notification.", "uri": "http://localhost:5240/MAAS/api/2.0/notifications/{id}/", "path": "/MAAS/api/2.0/notifications/{id}/"}, {"name": "FanNetworkHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found.", "op": null}], "doc": "Manage Fan Network.", "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/", "path": "/MAAS/api/2.0/fannetworks/{id}/"}, {"name": "PackageRepositoriesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all Package Repositories.", "op": null}], "doc": "Manage the collection of all Package Repositories in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/", "path": "/MAAS/api/2.0/package-repositories/"}, {"name": "VolumeGroupsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage volume groups on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/"}, {"name": "MachinesHandler", "params": [], "actions": [{"restful": false, "name": "add_chassis", "method": "POST", "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed.", "op": "add_chassis"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.", "op": "power_parameters"}, {"restful": false, "name": "accept_all", "method": "POST", "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.", "op": "accept_all"}, {"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}, {"restful": false, "name": "allocate", "method": "POST", "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound.", "op": "allocate"}, {"restful": true, "name": "create", "method": "POST", "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode", "op": null}, {"restful": false, "name": "list_allocated", "method": "GET", "doc": "Fetch Machines that were allocated to the User/oauth token.", "op": "list_allocated"}, {"restful": false, "name": "accept", "method": "POST", "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin.", "op": "accept"}, {"restful": false, "name": "release", "method": "POST", "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state.", "op": "release"}], "doc": "Manage the collection of all the machines in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "path": "/MAAS/api/2.0/machines/"}, {"name": "DiscoveryHandler", "params": ["discovery_id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}], "doc": "Read or delete an observed discovery.", "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/", "path": "/MAAS/api/2.0/discovery/{discovery_id}/"}, {"name": "NodeResultsHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable", "op": null}], "doc": "Read the collection of NodeResult in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/", "path": "/MAAS/api/2.0/installation-results/"}, {"name": "FanNetworksHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all fannetworks.", "op": null}], "doc": "Manage Fan Networks.", "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/", "path": "/MAAS/api/2.0/fannetworks/"}, {"name": "NotificationsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a notification.\n\nThis is available to admins *only*.\n\n:param message: The message for this notification. May contain basic\n HTML; this will be sanitised before display.\n:param context: Optional JSON context. The root object *must* be an\n object (i.e. a mapping). The values herein can be referenced by\n `message` with Python's \"format\" (not %) codes.\n:param category: Optional category. Choose from: error, warning,\n success, or info. Defaults to info.\n\n:param ident: Optional unique identifier for this notification.\n:param user: Optional user ID this notification is intended for. By\n default it will not be targeted to any individual user.\n:param users: Optional boolean, true to notify all users, defaults to\n false, i.e. not targeted to all users.\n:param admins: Optional boolean, true to notify all admins, defaults to\n false, i.e. not targeted to all admins.\n\nNote: if neither user nor users nor admins is set, the notification\nwill not be seen by anyone.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List notifications relevant to the invoking user.\n\nNotifications that have been dismissed are *not* returned.", "op": null}], "doc": "Manage the collection of all the notifications in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/notifications/", "path": "/MAAS/api/2.0/notifications/"}, {"name": "DNSResourceRecordHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.", "op": null}], "doc": "Manage dnsresourcerecord.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/", "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/"}, {"name": "RaidHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage a specific RAID device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/"}, {"name": "RackControllerHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "import_boot_images", "method": "POST", "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found.", "op": "import_boot_images"}, {"restful": false, "name": "test", "method": "POST", "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.", "op": "test"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}, {"restful": false, "name": "power_off", "method": "POST", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.", "op": "power_off"}, {"restful": false, "name": "list_boot_images", "method": "GET", "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found.", "op": "list_boot_images"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller.", "op": null}, {"restful": false, "name": "power_on", "method": "POST", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "power_on"}, {"restful": false, "name": "abort", "method": "POST", "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.", "op": "abort"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "query_power_state", "method": "GET", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.", "op": "query_power_state"}], "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/", "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/"}, {"name": "SSHKeyHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.", "op": null}], "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/"}, {"name": "VlanHandler", "params": ["fabric_id", "vid"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n:param relay_vlan: Only set when this VLAN will be using a DHCP relay\n to forward DHCP requests to another VLAN that MAAS is or will run\n the DHCP server. MAAS will not run the DHCP relay itself, it must\n be configured to proxy reqests to the primary and/or secondary\n rack controller interfaces for the VLAN specified in this field.\n:type relay_vlan: ID of VLAN\n:param space: The space this VLAN should be placed in. Passing in an\n empty string (or the string 'undefined') will cause the VLAN to be\n placed in the 'undefined' space.\n:type space: unicode\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}], "doc": "Manage VLAN on a fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/"}, {"name": "CommissioningScriptHandler", "params": ["name"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a commissioning script.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a commissioning script.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a commissioning script.", "op": null}], "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.", "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}", "path": "/MAAS/api/2.0/commissioning-scripts/{name}"}, {"name": "DNSResourceRecordsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.", "op": null}], "doc": "Manage dnsresourcerecords.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/", "path": "/MAAS/api/2.0/dnsresourcerecords/"}, {"name": "RaidsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage all RAID devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/", "path": "/MAAS/api/2.0/nodes/{system_id}/raids/"}, {"name": "RackControllersHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.", "op": "power_parameters"}, {"restful": false, "name": "import_boot_images", "method": "POST", "doc": "Import the boot images on all rack controllers.", "op": "import_boot_images"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}, {"restful": false, "name": "describe_power_types", "method": "GET", "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format.", "op": "describe_power_types"}], "doc": "Manage the collection of all rack controllers in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/", "path": "/MAAS/api/2.0/rackcontrollers/"}, {"name": "AccountHandler", "params": [], "actions": [{"restful": false, "name": "list_authorisation_tokens", "method": "GET", "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token.", "op": "list_authorisation_tokens"}, {"restful": false, "name": "update_token_name", "method": "POST", "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode", "op": "update_token_name"}, {"restful": false, "name": "delete_authorisation_token", "method": "POST", "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode", "op": "delete_authorisation_token"}, {"restful": false, "name": "create_authorisation_token", "method": "POST", "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)", "op": "create_authorisation_token"}], "doc": "Manage the current logged-in user.", "uri": "http://localhost:5240/MAAS/api/2.0/account/", "path": "/MAAS/api/2.0/account/"}, {"name": "SSHKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all keys belonging to the requesting user.", "op": null}, {"restful": false, "name": "import", "method": "POST", "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format.", "op": "import"}], "doc": "Manage the collection of all the SSH keys in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/", "path": "/MAAS/api/2.0/account/prefs/sshkeys/"}, {"name": "VlansHandler", "params": ["fabric_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found.", "op": null}], "doc": "Manage VLANs on a fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/"}, {"name": "BootResourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot resource.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete boot resource.", "op": null}], "doc": "Manage a boot resource.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/", "path": "/MAAS/api/2.0/boot-resources/{id}/"}, {"name": "CommissioningScriptsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List commissioning scripts.", "op": null}], "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.", "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/", "path": "/MAAS/api/2.0/commissioning-scripts/"}, {"name": "DiscoveriesHandler", "params": [], "actions": [{"restful": false, "name": "by_unknown_ip", "method": "GET", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_ip"}, {"restful": false, "name": "scan", "method": "POST", "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU.", "op": "scan"}, {"restful": false, "name": "by_unknown_mac", "method": "GET", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_mac"}, {"restful": false, "name": "by_unknown_ip_and_mac", "method": "GET", "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_ip_and_mac"}, {"restful": false, "name": "clear", "method": "POST", "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data.", "op": "clear"}, {"restful": true, "name": "read", "method": "GET", "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": null}], "doc": "Query observed discoveries.", "uri": "http://localhost:5240/MAAS/api/2.0/discovery/", "path": "/MAAS/api/2.0/discovery/"}, {"name": "DNSResourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found.", "op": null}], "doc": "Manage dnsresource.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/", "path": "/MAAS/api/2.0/dnsresources/{id}/"}, {"name": "BcacheHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage bcache device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/"}, {"name": "RegionControllerHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}], "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/", "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/"}, {"name": "EventsHandler", "params": [], "actions": [{"restful": false, "name": "query", "method": "GET", "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events.", "op": "query"}], "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.", "uri": "http://localhost:5240/MAAS/api/2.0/events/", "path": "/MAAS/api/2.0/events/"}, {"name": "MaasHandler", "params": [], "actions": [{"restful": false, "name": "set_config", "method": "POST", "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)", "op": "set_config"}, {"restful": false, "name": "get_config", "method": "GET", "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)", "op": "get_config"}], "doc": "Manage the MAAS server.", "uri": "http://localhost:5240/MAAS/api/2.0/maas/", "path": "/MAAS/api/2.0/maas/"}, {"name": "SSLKeyHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.", "op": null}], "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/"}, {"name": "SpaceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read space.\n\nReturns 404 if the space is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete space.\n\nReturns 404 if the space is not found.", "op": null}], "doc": "Manage space.", "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/", "path": "/MAAS/api/2.0/spaces/{id}/"}, {"name": "NetworkHandler", "params": ["name"], "actions": [{"restful": false, "name": "disconnect_macs", "method": "POST", "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": "disconnect_macs"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": null}, {"restful": false, "name": "connect_macs", "method": "POST", "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": "connect_macs"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read network definition.", "op": null}, {"restful": false, "name": "list_connected_macs", "method": "GET", "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned.", "op": "list_connected_macs"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators.", "op": null}], "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.", "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/", "path": "/MAAS/api/2.0/networks/{name}/"}, {"name": "BootResourcesHandler", "params": [], "actions": [{"restful": false, "name": "stop_import", "method": "POST", "doc": "Stop import of boot resources.", "op": "stop_import"}, {"restful": true, "name": "create", "method": "POST", "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload.", "op": null}, {"restful": false, "name": "is_importing", "method": "GET", "doc": "Return import status.", "op": "is_importing"}, {"restful": true, "name": "read", "method": "GET", "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all", "op": null}, {"restful": false, "name": "import", "method": "POST", "doc": "Import the boot resources.", "op": "import"}], "doc": "Manage the boot resources.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/", "path": "/MAAS/api/2.0/boot-resources/"}, {"name": "DNSResourcesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.", "op": null}], "doc": "Manage dnsresources.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/", "path": "/MAAS/api/2.0/dnsresources/"}, {"name": "BootSourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot source.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific boot source.", "op": null}], "doc": "Manage a boot source.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/", "path": "/MAAS/api/2.0/boot-sources/{id}/"}, {"name": "BcachesHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage bcache devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/"}, {"name": "RegionControllersHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all region controllers in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/", "path": "/MAAS/api/2.0/regioncontrollers/"}, {"name": "FilesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied", "op": null}, {"restful": false, "name": "get", "method": "GET", "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.", "op": "get"}, {"restful": true, "name": "read", "method": "GET", "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode", "op": null}, {"restful": false, "name": "get_by_key", "method": "GET", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.", "op": "get_by_key"}], "doc": "Manage the collection of all the files in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/files/", "path": "/MAAS/api/2.0/files/"}, {"name": "SSLKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all keys belonging to the requesting user.", "op": null}], "doc": "Operations on multiple keys.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/", "path": "/MAAS/api/2.0/account/prefs/sslkeys/"}, {"name": "SpacesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all spaces.", "op": null}], "doc": "Manage spaces.", "uri": "http://localhost:5240/MAAS/api/2.0/spaces/", "path": "/MAAS/api/2.0/spaces/"}, {"name": "BootSourcesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List boot sources.\n\nGet a listing of boot sources.", "op": null}], "doc": "Manage the collection of boot sources.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/", "path": "/MAAS/api/2.0/boot-sources/"}, {"name": "LicenseKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List license keys.", "op": null}], "doc": "Manage the license keys.", "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/", "path": "/MAAS/api/2.0/license-keys/"}, {"name": "DomainHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read domain.\n\nReturns 404 if the domain is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.", "op": null}], "doc": "Manage domain.", "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/", "path": "/MAAS/api/2.0/domains/{id}/"}, {"name": "BcacheCacheSetHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage bcache cache set on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/"}, {"name": "DeviceHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "set_owner_data", "method": "POST", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.", "op": "set_owner_data"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted.", "op": null}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device.", "op": "restore_default_configuration"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device.", "op": null}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device.", "op": "restore_networking_configuration"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}], "doc": "Manage an individual device.\n\nThe device is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/", "path": "/MAAS/api/2.0/devices/{system_id}/"}, {"name": "FileHandler", "params": ["filename"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a FileStorage object.", "op": null}], "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.", "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/", "path": "/MAAS/api/2.0/files/{filename}/"}, {"name": "UserHandler", "params": ["username"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Deletes a user", "op": null}], "doc": "Manage a user account.", "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/", "path": "/MAAS/api/2.0/users/{username}/"}, {"name": "SubnetHandler", "params": ["id"], "actions": [{"restful": false, "name": "ip_addresses", "method": "GET", "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional parameters\n-------------------\n\nwith_username\n If False, suppresses the display of usernames associated with each\n address. (Default: True)\n\nwith_node_summary\n If False, suppresses the display of any node associated with each\n address. (Default: True)", "op": "ip_addresses"}, {"restful": false, "name": "reserved_ip_ranges", "method": "GET", "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found.", "op": "reserved_ip_ranges"}, {"restful": false, "name": "unreserved_ip_ranges", "method": "GET", "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found.", "op": "unreserved_ip_ranges"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read subnet.\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update the specified subnet.\n\nPlease see the documentation for the 'create' operation for detailed\ndescriptions of each parameter.\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to.\n\nspace\n Space this subnet is in.\n\ncidr\n The network CIDR for this subnet.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n\nallow_proxy\n Configure maas-proxy to allow requests from this subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n If False, MAAS should not manage this subnet. (Default: True)\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": false, "name": "statistics", "method": "GET", "doc": "Returns statistics for the specified subnet, including:\n\nnum_available: the number of available IP addresses\nlargest_available: the largest number of contiguous free IP addresses\nnum_unavailable: the number of unavailable IP addresses\ntotal_addresses: the sum of the available plus unavailable addresses\nusage: the (floating point) usage percentage of this subnet\nusage_string: the (formatted unicode) usage percentage of this subnet\nranges: the specific IP ranges present in ths subnet (if specified)\n\nOptional parameters\n-------------------\n\ninclude_ranges\n If True, includes detailed information\n about the usage of this range.\n\ninclude_suggestions\n If True, includes the suggested gateway and dynamic range for this\n subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found.", "op": "statistics"}], "doc": "Manage subnet.", "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/", "path": "/MAAS/api/2.0/subnets/{id}/"}, {"name": "BootSourceSelectionHandler", "params": ["boot_source_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot source selection.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific boot source.", "op": null}], "doc": "Manage a boot source selection.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/"}, {"name": "DomainsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all domains.", "op": null}, {"restful": false, "name": "set_serial", "method": "POST", "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next.", "op": "set_serial"}], "doc": "Manage domains.", "uri": "http://localhost:5240/MAAS/api/2.0/domains/", "path": "/MAAS/api/2.0/domains/"}, {"name": "BcacheCacheSetsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage bcache cache sets on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/"}, {"name": "DevicesHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "create", "method": "POST", "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all the devices in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/devices/", "path": "/MAAS/api/2.0/devices/"}, {"name": "UsersHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing.", "op": null}, {"restful": false, "name": "whoami", "method": "GET", "doc": "Returns the currently logged in user.", "op": "whoami"}, {"restful": true, "name": "read", "method": "GET", "doc": "List users.", "op": null}], "doc": "Manage the user accounts of this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/users/", "path": "/MAAS/api/2.0/users/"}, {"name": "BootSourceSelectionsHandler", "params": ["boot_source_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List boot source selections.\n\nGet a listing of a boot source's selections.", "op": null}], "doc": "Manage the collection of boot source selections.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/"}, {"name": "SubnetsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a subnet.\n\nRequired parameters\n-------------------\n\ncidr\n The network CIDR for this subnet.\n\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to. Defaults to the default VLAN for the\n provided fabric or defaults to the default VLAN in the default fabric\n (if unspecified).\n\nfabric\n Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to, or defaults to the default fabric.\n\nvid\n VID of the VLAN this subnet belongs to. Only used when vlan is\n not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n\nspace\n Space this subnet is in. Defaults to the default space.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled\n means no reverse zone is created; Enabled means generate the\n reverse zone; RFC2317 extends Enabled to create the necessary\n parent zone with the appropriate CNAME resource records for the\n network, if the network is small enough to require the support\n described in RFC2317.\n\nallow_proxy\n Configure maas-proxy to allow requests from this\n subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n In MAAS 2.0+, all subnets are assumed to be managed by default.\n\n Only managed subnets allow DHCP to be enabled on their related\n dynamic ranges. (Thus, dynamic ranges become \"informational\n only\"; an indication that another DHCP server is currently\n handling them, or that MAAS will handle them when the subnet is\n enabled for management.)\n\n Managed subnets do not allow IP allocation by default. The\n meaning of a \"reserved\" IP range is reversed for an unmanaged\n subnet. (That is, for managed subnets, \"reserved\" means \"MAAS\n cannot allocate any IP address within this reserved block\". For\n unmanaged subnets, \"reserved\" means \"MAAS must allocate IP\n addresses only from reserved IP ranges\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all subnets.", "op": null}], "doc": "Manage subnets.", "uri": "http://localhost:5240/MAAS/api/2.0/subnets/", "path": "/MAAS/api/2.0/subnets/"}, {"name": "BlockDevicesHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage block devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/"}, {"name": "InterfaceHandler", "params": ["system_id", "id"], "actions": [{"restful": false, "name": "link_subnet", "method": "POST", "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.", "op": "link_subnet"}, {"restful": false, "name": "remove_tag", "method": "POST", "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.", "op": "remove_tag"}, {"restful": false, "name": "unlink_subnet", "method": "POST", "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.", "op": "unlink_subnet"}, {"restful": false, "name": "set_default_gateway", "method": "POST", "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.", "op": "set_default_gateway"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": false, "name": "disconnect", "method": "POST", "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found.", "op": "disconnect"}, {"restful": false, "name": "add_tag", "method": "POST", "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.", "op": "add_tag"}], "doc": "Manage a node's or device's interface.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/"}, {"name": "PodHandler", "params": ["id"], "actions": [{"restful": false, "name": "parameters", "method": "GET", "doc": "Obtain pod parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the pod parameters, if any, configured for a\npod. For some types of pod this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the pod is not found.", "op": "parameters"}, {"restful": false, "name": "refresh", "method": "POST", "doc": "Refresh a specific Pod.\n\nPerforms pod discovery and updates all discovered information and\ndiscovered machines.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to refresh the pod.", "op": "refresh"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Pod.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to delete the pod.\nReturns 204 if the pod is successfully deleted.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}, {"restful": false, "name": "compose", "method": "POST", "doc": "Compose a machine from Pod.\n\nAll fields below are optional:\n\n:param cores: Minimum number of CPU cores.\n:param memory: Minimum amount of memory (MiB).\n:param cpu_speed: Minimum amount of CPU speed (MHz).\n:param architecture: Architecture for the machine. Must be an\n architecture that the pod supports.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to compose machine.", "op": "compose"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Pod.\n\n:param name: Name for the pod (optional).\n\nNote: 'type' cannot be updated on a Pod. The Pod must be deleted and\nre-added to change the type.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to update the pod.", "op": null}], "doc": "Manage an individual pod.\n\nThe pod is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/pods/{id}/", "path": "/MAAS/api/2.0/pods/{id}/"}, {"name": "ZoneHandler", "params": ["name"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted.", "op": null}], "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.", "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/", "path": "/MAAS/api/2.0/zones/{name}/"}, {"name": "IPAddressesHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode", "op": null}, {"restful": false, "name": "reserve", "method": "POST", "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available.", "op": "reserve"}, {"restful": false, "name": "release", "method": "POST", "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found.", "op": "release"}], "doc": "Manage IP addresses allocated by MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/", "path": "/MAAS/api/2.0/ipaddresses/"}, {"name": "IPRangeHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read IP range.\n\nReturns 404 if the IP range is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found.", "op": null}], "doc": "Manage IP range.", "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/", "path": "/MAAS/api/2.0/ipranges/{id}/"}, {"name": "LicenseKeyHandler", "params": ["osystem", "distro_series"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read license key.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete license key.", "op": null}], "doc": "Manage a license key.", "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}"}, {"name": "BlockDeviceHandler", "params": ["system_id", "id"], "actions": [{"restful": false, "name": "remove_tag", "method": "POST", "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": "remove_tag"}, {"restful": false, "name": "mount", "method": "POST", "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "mount"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": false, "name": "set_boot_disk", "method": "POST", "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated.", "op": "set_boot_disk"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found.", "op": null}, {"restful": false, "name": "unformat", "method": "POST", "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "unformat"}, {"restful": false, "name": "format", "method": "POST", "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "format"}, {"restful": false, "name": "add_tag", "method": "POST", "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": "add_tag"}, {"restful": false, "name": "unmount", "method": "POST", "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "unmount"}], "doc": "Manage a block device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/"}, {"name": "InterfacesHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "create_bridge", "method": "POST", "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_bridge"}, {"restful": false, "name": "create_physical", "method": "POST", "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_physical"}, {"restful": false, "name": "create_vlan", "method": "POST", "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_vlan"}, {"restful": true, "name": "read", "method": "GET", "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "create_bond", "method": "POST", "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_bond"}], "doc": "Manage interfaces on a node.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/", "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/"}, {"name": "ZonesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List zones.\n\nGet a listing of all the physical zones.", "op": null}], "doc": "Manage physical zones.", "uri": "http://localhost:5240/MAAS/api/2.0/zones/", "path": "/MAAS/api/2.0/zones/"}, {"name": "IPRangesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all IP ranges.", "op": null}], "doc": "Manage IP ranges.", "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/", "path": "/MAAS/api/2.0/ipranges/"}], "resources": [{"name": "NetworksHandler", "anon": null, "auth": {"name": "NetworksHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Define a network.\n\nThis endpoint is no longer available. Use the 'subnets' endpoint\ninstead.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List networks.\n\n:param node: Optionally, nodes which must be attached to any returned\n networks. If more than one node is given, the result will be\n restricted to networks that these nodes have in common.", "op": null}], "doc": "Manage the networks.\n\nThis endpoint is deprecated. Use the new 'subnets' endpoint instead.", "uri": "http://localhost:5240/MAAS/api/2.0/networks/", "path": "/MAAS/api/2.0/networks/"}}, {"name": "TagHandler", "anon": null, "auth": {"name": "TagHandler", "params": ["name"], "actions": [{"restful": false, "name": "update_nodes", "method": "POST", "doc": "Add or remove nodes being associated with this tag.\n\n:param add: system_ids of nodes to add to this tag.\n:param remove: system_ids of nodes to remove from this tag.\n:param definition: (optional) If supplied, the definition will be\n validated against the current definition of the tag. If the value\n does not match, then the update will be dropped (assuming this was\n just a case of a worker being out-of-date)\n:param rack_controller: A system ID of a rack controller that did the\n processing. This value is optional. If not supplied, the requester\n must be a superuser. If supplied, then the requester must be the\n rack controller.\n\nReturns 404 if the tag is not found.\nReturns 401 if the user does not have permission to update the nodes.\nReturns 409 if 'definition' doesn't match the current definition.", "op": "update_nodes"}, {"restful": false, "name": "rebuild", "method": "POST", "doc": "Manually trigger a rebuild the tag <=> node mapping.\n\nThis is considered a maintenance operation, which should normally not\nbe necessary. Adding nodes or updating a tag's definition should\nautomatically trigger the appropriate changes.\n\nReturns 404 if the tag is not found.", "op": "rebuild"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Tag.\n\nReturns 404 if the tag is not found.", "op": null}, {"restful": false, "name": "rack_controllers", "method": "GET", "doc": "Get the list of rack controllers that have this tag.\n\nReturns 404 if the tag is not found.", "op": "rack_controllers"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Tag.\n\nReturns 404 if the tag is not found.\nReturns 204 if the tag is successfully deleted.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n\nReturns 404 if the tag is not found.", "op": null}, {"restful": false, "name": "machines", "method": "GET", "doc": "Get the list of machines that have this tag.\n\nReturns 404 if the tag is not found.", "op": "machines"}, {"restful": false, "name": "devices", "method": "GET", "doc": "Get the list of devices that have this tag.\n\nReturns 404 if the tag is not found.", "op": "devices"}, {"restful": false, "name": "region_controllers", "method": "GET", "doc": "Get the list of region controllers that have this tag.\n\nReturns 404 if the tag is not found.", "op": "region_controllers"}, {"restful": false, "name": "nodes", "method": "GET", "doc": "Get the list of nodes that have this tag.\n\nReturns 404 if the tag is not found.", "op": "nodes"}], "doc": "Manage a Tag.\n\nTags are properties that can be associated with a Node and serve as\ncriteria for selecting and allocating nodes.\n\nA Tag is identified by its name.", "uri": "http://localhost:5240/MAAS/api/2.0/tags/{name}/", "path": "/MAAS/api/2.0/tags/{name}/"}}, {"name": "DHCPSnippetHandler", "anon": null, "auth": {"name": "DHCPSnippetHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read DHCP snippet.\n\nReturns 404 if the snippet is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a DHCP snippet.\n\n:param name: The name of the DHCP snippet.\n:type name: unicode\n\n:param value: The new value of the DHCP snippet to be used in\n dhcpd.conf. Previous values are stored and can be reverted.\n:type value: unicode\n\n:param description: A description of what the DHCP snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the DHCP snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node the DHCP snippet is to be used for. Can not be\n set if subnet is set.\n:type node: unicode\n\n:param subnet: The subnet the DHCP snippet is to be used for. Can not\n be set if node is set.\n:type subnet: unicode\n\n:param global_snippet: Set the DHCP snippet to be a global option. This\n removes any node or subnet links.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a DHCP snippet.\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": false, "name": "revert", "method": "POST", "doc": "Revert the value of a DHCP snippet to an earlier revision.\n\n:param to: What revision in the DHCP snippet's history to revert to.\n This can either be an ID or a negative number representing how far\n back to go.\n:type to: integer\n\nReturns 404 if the DHCP snippet is not found.", "op": "revert"}], "doc": "Manage an individual DHCP snippet.\n\nThe DHCP snippet is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/{id}/", "path": "/MAAS/api/2.0/dhcp-snippets/{id}/"}}, {"name": "PartitionHandler", "anon": null, "auth": {"name": "PartitionHandler", "params": ["system_id", "device_id", "id"], "actions": [{"restful": false, "name": "mount", "method": "POST", "doc": "Mount the filesystem on partition.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "mount"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete partition.\n\nReturns 404 if the node, block device, or partition are not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read partition.\n\nReturns 404 if the node, block device, or partition are not found.", "op": null}, {"restful": false, "name": "unformat", "method": "POST", "doc": "Unformat a partition.", "op": "unformat"}, {"restful": false, "name": "format", "method": "POST", "doc": "Format a partition.\n\n:param fstype: Type of filesystem.\n:param uuid: The UUID for the filesystem.\n:param label: The label for the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "format"}, {"restful": false, "name": "unmount", "method": "POST", "doc": "Unmount the filesystem on partition.\n\nReturns 400 if the partition is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the partition.\nReturns 404 if the node, block device, or partition is not found.", "op": "unmount"}], "doc": "Manage partition on a block device.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partition/{id}"}}, {"name": "StaticRouteHandler", "anon": null, "auth": {"name": "StaticRouteHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read static route.\n\nReturns 404 if the static route is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.\n\nReturns 404 if the static route is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete static route.\n\nReturns 404 if the static route is not found.", "op": null}], "doc": "Manage static route.", "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/{id}/", "path": "/MAAS/api/2.0/static-routes/{id}/"}}, {"name": "NodeHandler", "anon": null, "auth": {"name": "NodeHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}], "doc": "Manage an individual Node.\n\nThe Node is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/"}}, {"name": "PodsHandler", "anon": null, "auth": {"name": "PodsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a Pod.\n\n:param type: Type of pod to create.\n:param name: Name for the pod (optional).\n\nReturns 503 if the pod could not be discovered.\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to create a pod.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List pods.\n\nGet a listing of all the pods.", "op": null}], "doc": "Manage the collection of all the pod in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/pods/", "path": "/MAAS/api/2.0/pods/"}}, {"name": "FabricHandler", "anon": null, "auth": {"name": "FabricHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read fabric.\n\nReturns 404 if the fabric is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.\n\nReturns 404 if the fabric is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete fabric.\n\nReturns 404 if the fabric is not found.", "op": null}], "doc": "Manage fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{id}/", "path": "/MAAS/api/2.0/fabrics/{id}/"}}, {"name": "TagsHandler", "anon": null, "auth": {"name": "TagsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new Tag.\n\n:param name: The name of the Tag to be created. This should be a short\n name, and will be used in the URL of the tag.\n:param comment: A long form description of what the tag is meant for.\n It is meant as a human readable description of the tag.\n:param definition: An XPATH query that will be evaluated against the\n hardware_details stored for all nodes (output of `lshw -xml`).\n:param kernel_opts: Can be None. If set, nodes associated with this tag\n will add this string to their kernel options when booting. The\n value overrides the global 'kernel_opts' setting. If more than one\n tag is associated with a node, the one with the lowest alphabetical\n name will be picked (eg 01-my-tag will be taken over 99-tag-name).\n\nReturns 401 if the user is not an admin.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List Tags.\n\nGet a listing of all tags that are currently defined.", "op": null}], "doc": "Manage the collection of all the Tags in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/tags/", "path": "/MAAS/api/2.0/tags/"}}, {"name": "DHCPSnippetsHandler", "anon": null, "auth": {"name": "DHCPSnippetsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a DHCP snippet.\n\n:param name: The name of the DHCP snippet. This is required to create\n a new DHCP snippet.\n:type name: unicode\n\n:param value: The snippet of config inserted into dhcpd.conf. This is\n required to create a new DHCP snippet.\n:type value: unicode\n\n:param description: A description of what the snippet does.\n:type description: unicode\n\n:param enabled: Whether or not the snippet is currently enabled.\n:type enabled: boolean\n\n:param node: The node this snippet applies to. Cannot be used with\n subnet or global_snippet.\n:type node: unicode\n\n:param subnet: The subnet this snippet applies to. Cannot be used with\n node or global_snippet.\n:type subnet: unicode\n\n:param global_snippet: Whether or not this snippet is to be applied\n globally. Cannot be used with node or subnet.\n:type global_snippet: boolean\n\nReturns 404 if the DHCP snippet is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all DHCP snippets.", "op": null}], "doc": "Manage the collection of all DHCP snippets in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/dhcp-snippets/", "path": "/MAAS/api/2.0/dhcp-snippets/"}}, {"name": "PartitionsHandler", "anon": null, "auth": {"name": "PartitionsHandler", "params": ["system_id", "device_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a partition on the block device.\n\n:param size: The size of the partition.\n:param uuid: UUID for the partition. Only used if the partition table\n type for the block device is GPT.\n:param bootable: If the partition should be marked bootable.\n\nReturns 404 if the node or the block device are not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all partitions on the block device.\n\nReturns 404 if the node or the block device are not found.", "op": null}], "doc": "Manage partitions on a block device.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{device_id}/partitions/"}}, {"name": "NodesHandler", "anon": {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, "auth": {"name": "NodesHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all the nodes in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}}, {"name": "StaticRoutesHandler", "anon": null, "auth": {"name": "StaticRoutesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a static route.\n\n:param source: Source subnet for the route.\n:param destination: Destination subnet for the route.\n:param gateway_ip: IP address of the gateway on the source subnet.\n:param metric: Weight of the route on a deployed machine.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all static routes.", "op": null}], "doc": "Manage static routes.", "uri": "http://localhost:5240/MAAS/api/2.0/static-routes/", "path": "/MAAS/api/2.0/static-routes/"}}, {"name": "FabricsHandler", "anon": null, "auth": {"name": "FabricsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a fabric.\n\n:param name: Name of the fabric.\n:param description: Description of the fabric.\n:param class_type: Class type of the fabric.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all fabrics.", "op": null}], "doc": "Manage fabrics.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/", "path": "/MAAS/api/2.0/fabrics/"}}, {"name": "VersionHandler", "anon": {"name": "VersionHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Version and capabilities of this MAAS instance.", "op": null}], "doc": "Information about this MAAS instance.\n\nThis returns a JSON dictionary with information about this\nMAAS instance::\n\n {\n 'version': '1.8.0',\n 'subversion': 'alpha10+bzr3750',\n 'capabilities': ['capability1', 'capability2', ...]\n }", "uri": "http://localhost:5240/MAAS/api/2.0/version/", "path": "/MAAS/api/2.0/version/"}, "auth": null}, {"name": "PackageRepositoryHandler", "anon": null, "auth": {"name": "PackageRepositoryHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read Package Repository.\n\nReturns 404 if the repository is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean\n\nReturns 404 if the Package Repository is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a Package Repository.\n\nReturns 404 if the Package Repository is not found.", "op": null}], "doc": "Manage an individual Package Repository.\n\nThe Package Repository is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/{id}/", "path": "/MAAS/api/2.0/package-repositories/{id}/"}}, {"name": "MachineHandler", "anon": null, "auth": {"name": "MachineHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_off", "method": "POST", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.", "op": "power_off"}, {"restful": false, "name": "mount_special", "method": "POST", "doc": "Mount a special-purpose filesystem, like tmpfs.\n\n:param fstype: The filesystem type. This must be a filesystem that\n does not require a block special device.\n:param mount_point: Path on the filesystem to mount.\n:param mount_option: Options to pass to mount(8).\n\nReturns 403 when the user is not permitted to mount the partition.", "op": "mount_special"}, {"restful": false, "name": "unmount_special", "method": "POST", "doc": "Unmount a special-purpose filesystem, like tmpfs.\n\n:param mount_point: Path on the filesystem to unmount.\n\nReturns 403 when the user is not permitted to unmount the partition.", "op": "unmount_special"}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "doc": "Reset a machine's configuration to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_default_configuration"}, {"restful": false, "name": "mark_fixed", "method": "POST", "doc": "Mark a broken node as fixed and set its status as 'ready'.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to mark the machine\nfixed.", "op": "mark_fixed"}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "doc": "Reset a machine's networking options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_networking_configuration"}, {"restful": false, "name": "mark_broken", "method": "POST", "doc": "Mark a node as 'broken'.\n\nIf the node is allocated, release it first.\n\n:param comment: Optional comment for the event log. Will be\n displayed on the node as an error description until marked fixed.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to mark the node\nbroken.", "op": "mark_broken"}, {"restful": false, "name": "commission", "method": "POST", "doc": "Begin commissioning process for a machine.\n\n:param enable_ssh: Whether to enable SSH for the commissioning\n environment using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param skip_networking: Whether to skip re-configuring the networking\n on the machine after the commissioning has completed.\n:type skip_networking: bool ('0' for False, '1' for True)\n:param skip_storage: Whether to skip re-configuring the storage\n on the machine after the commissioning has completed.\n:type skip_storage: bool ('0' for False, '1' for True)\n:param commissioning_scripts: A comma seperated list of commissioning\n script names and tags to be run. By default all custom\n commissioning scripts are run. Builtin commissioning scripts always\n run.\n:type commissioning_scripts: string\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run. Set to 'none' to disable running tests.\n:type testing_scripts: string\n\nA machine in the 'ready', 'declared' or 'failed test' state may\ninitiate a commissioning cycle where it is checked out and tested\nin preparation for transitioning to the 'ready' state. If it is\nalready in the 'ready' state this is considered a re-commissioning\nprocess which is useful if commissioning tests were changed after\nit previously commissioned.\n\nReturns 404 if the machine is not found.", "op": "commission"}, {"restful": false, "name": "test", "method": "POST", "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.", "op": "test"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "release", "method": "POST", "doc": "Release a machine. Opposite of `Machines.allocate`.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param erase: Erase the disk when releasing.\n:type erase: boolean\n:param secure_erase: Use the drive's secure erase feature if available.\n In some cases this can be much faster than overwriting the drive.\n Some drives implement secure erasure by overwriting themselves so\n this could still be slow.\n:type secure_erase: boolean\n:param quick_erase: Wipe 1MiB at the start and at the end of the drive\n to make data recovery inconvenient and unlikely to happen by\n accident. This is not secure.\n:type quick_erase: boolean\n\nIf neither secure_erase nor quick_erase are specified, MAAS will\noverwrite the whole disk with null bytes. This can be very slow.\n\nIf both secure_erase and quick_erase are specified and the drive does\nNOT have a secure erase feature, MAAS will behave as if only\nquick_erase was specified.\n\nIf secure_erase is specified and quick_erase is NOT specified and the\ndrive does NOT have a secure erase feature, MAAS will behave as if\nsecure_erase was NOT specified, i.e. will overwrite the whole disk\nwith null bytes. This can be very slow.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user doesn't have permission to release the machine.\nReturns 409 if the machine is in a state where it may not be released.", "op": "release"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "restore_storage_configuration", "method": "POST", "doc": "Reset a machine's storage options to its initial state.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to reset the machine.", "op": "restore_storage_configuration"}, {"restful": false, "name": "set_owner_data", "method": "POST", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.", "op": "set_owner_data"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}, {"restful": false, "name": "clear_default_gateways", "method": "POST", "doc": "Clear any set default gateways on the machine.\n\nThis will clear both IPv4 and IPv6 gateways on the machine. This will\ntransition the logic of identifing the best gateway to MAAS. This logic\nis determined based the following criteria:\n\n1. Managed subnets over unmanaged subnets.\n2. Bond interfaces over physical interfaces.\n3. Machine's boot interface over all other interfaces except bonds.\n4. Physical interfaces over VLAN interfaces.\n5. Sticky IP links over user reserved IP links.\n6. User reserved IP links over auto IP links.\n\nIf the default gateways need to be specific for this machine you can\nset which interface and subnet's gateway to use when this machine is\ndeployed with the `interfaces set-default-gateway` API.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to clear the default\ngateways.", "op": "clear_default_gateways"}, {"restful": false, "name": "exit_rescue_mode", "method": "POST", "doc": "Exit rescue mode process for a machine.\n\nA machine in the 'rescue mode' state may exit the rescue mode\nprocess.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to exit the\nrescue mode process for this machine.", "op": "exit_rescue_mode"}, {"restful": false, "name": "rescue_mode", "method": "POST", "doc": "Begin rescue mode process for a machine.\n\nA machine in the 'deployed' or 'broken' state may initiate the\nrescue mode process.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the\nrescue mode process for this machine.", "op": "rescue_mode"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Machine.\n\n:param hostname: The new hostname for this machine.\n:type hostname: unicode\n\n:param domain: The domain for this machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param architecture: The new architecture for this machine.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param power_type: The new power type for this machine. If you use the\n default value, power_parameters will be set to the empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the Machine's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this machine should be checked against the expected\n power parameters for the machine's power type ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n machine.\n:type zone: unicode\n\n:param swap_size: Specifies the size of the swap file, in bytes. Field\n accept K, M, G and T suffixes for values expressed respectively in\n kilobytes, megabytes, gigabytes and terabytes.\n:type swap_size: unicode\n\n:param disable_ipv4: Deprecated. If specified, must be False.\n:type disable_ipv4: boolean\n\n:param cpu_count: The amount of CPU cores the machine has.\n:type cpu_count: integer\n\n:param memory: How much memory the machine has.\n:type memory: unicode\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to update the machine.", "op": null}, {"restful": false, "name": "get_curtin_config", "method": "GET", "doc": "Return the rendered curtin configuration for the machine.\n\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to get the curtin\nconfiguration.", "op": "get_curtin_config"}, {"restful": false, "name": "deploy", "method": "POST", "doc": "Deploy an operating system to a machine.\n\n:param user_data: If present, this blob of user-data to be made\n available to the machines through the metadata service.\n:type user_data: base64-encoded unicode\n:param distro_series: If present, this parameter specifies the\n OS release the machine will use.\n:type distro_series: unicode\n:param hwe_kernel: If present, this parameter specified the kernel to\n be used on the machine\n:type hwe_kernel: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "deploy"}, {"restful": false, "name": "power_on", "method": "POST", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "power_on"}, {"restful": false, "name": "abort", "method": "POST", "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.", "op": "abort"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": false, "name": "set_storage_layout", "method": "POST", "doc": "Changes the storage layout on the machine.\n\nThis can only be preformed on an allocated machine.\n\nNote: This will clear the current storage layout and any extra\nconfiguration and replace it will the new layout.\n\n:param storage_layout: Storage layout for the machine. (flat, lvm,\n and bcache)\n\nThe following are optional for all layouts:\n\n:param boot_size: Size of the boot partition.\n:param root_size: Size of the root partition.\n:param root_device: Physical block device to place the root partition.\n\nThe following are optional for LVM:\n\n:param vg_name: Name of created volume group.\n:param lv_name: Name of created logical volume.\n:param lv_size: Size of created logical volume.\n\nThe following are optional for Bcache:\n\n:param cache_device: Physical block device to use as the cache device.\n:param cache_mode: Cache mode for bcache device. (writeback,\n writethrough, writearound)\n:param cache_size: Size of the cache partition to create on the cache\n device.\n:param cache_no_part: Don't create a partition on the cache device.\n Use the entire disk as the cache device.\n\nReturns 400 if the machine is currently not allocated.\nReturns 404 if the machine could not be found.\nReturns 403 if the user does not have permission to set the storage\nlayout.", "op": "set_storage_layout"}, {"restful": false, "name": "query_power_state", "method": "GET", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.", "op": "query_power_state"}], "doc": "Manage an individual Machine.\n\nThe Machine is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/{system_id}/", "path": "/MAAS/api/2.0/machines/{system_id}/"}}, {"name": "VolumeGroupHandler", "anon": null, "auth": {"name": "VolumeGroupHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.", "op": null}, {"restful": false, "name": "create_logical_volume", "method": "POST", "doc": "Create a logical volume in the volume group.\n\n:param name: Name of the logical volume.\n:param uuid: (optional) UUID of the logical volume.\n:param size: Size of the logical volume.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": "create_logical_volume"}, {"restful": false, "name": "delete_logical_volume", "method": "POST", "doc": "Delete a logical volume in the volume group.\n\n:param id: ID of the logical volume.\n\nReturns 403 if no logical volume with id.\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": "delete_logical_volume"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Read volume group on a machine.\n\n:param name: Name of the volume group.\n:param uuid: UUID of the volume group.\n:param add_block_devices: Block devices to add to the volume group.\n:param remove_block_devices: Block devices to remove from the\n volume group.\n:param add_partitions: Partitions to add to the volume group.\n:param remove_partitions: Partitions to remove from the volume group.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete volume group on a machine.\n\nReturns 404 if the machine or volume group is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage volume group on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/volume-group/{id}/"}}, {"name": "NotificationHandler", "anon": null, "auth": {"name": "NotificationHandler", "params": ["id"], "actions": [{"restful": false, "name": "dismiss", "method": "POST", "doc": "Dismiss a specific notification.\n\nReturns HTTP 403 FORBIDDEN if this notification is not relevant\n(targeted) to the invoking user.\n\nIt is safe to call multiple times for the same notification.", "op": "dismiss"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific notification.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific notification.\n\nSee `NotificationsHandler.create` for field information.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific notification.", "op": null}], "doc": "Manage an individual notification.", "uri": "http://localhost:5240/MAAS/api/2.0/notifications/{id}/", "path": "/MAAS/api/2.0/notifications/{id}/"}}, {"name": "FanNetworkHandler", "anon": null, "auth": {"name": "FanNetworkHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read fannetwork.\n\nReturns 404 if the fannetwork is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.\n\nReturns 404 if the fannetwork is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete fannetwork.\n\nReturns 404 if the fannetwork is not found.", "op": null}], "doc": "Manage Fan Network.", "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/{id}/", "path": "/MAAS/api/2.0/fannetworks/{id}/"}}, {"name": "PackageRepositoriesHandler", "anon": null, "auth": {"name": "PackageRepositoriesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a Package Repository.\n\n:param name: The name of the Package Repository.\n:type name: unicode\n\n:param url: The url of the Package Repository.\n:type url: unicode\n\n:param distributions: Which package distributions to include.\n:type distributions: unicode\n\n:param disabled_pockets: The list of pockets to disable.\n\n:param components: The list of components to enable.\n\n:param arches: The list of supported architectures.\n\n:param key: The authentication key to use with the repository.\n:type key: unicode\n\n:param enabled: Whether or not the repository is enabled.\n:type enabled: boolean", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all Package Repositories.", "op": null}], "doc": "Manage the collection of all Package Repositories in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/package-repositories/", "path": "/MAAS/api/2.0/package-repositories/"}}, {"name": "VolumeGroupsHandler", "anon": null, "auth": {"name": "VolumeGroupsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a volume group belonging to machine.\n\n:param name: Name of the volume group.\n:param uuid: (optional) UUID of the volume group.\n:param block_devices: Block devices to add to the volume group.\n:param partitions: Partitions to add to the volume group.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all volume groups belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage volume groups on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/volume-groups/", "path": "/MAAS/api/2.0/nodes/{system_id}/volume-groups/"}}, {"name": "MachinesHandler", "anon": {"name": "AnonMachinesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new Machine.\n\nAdding a server to a MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type:unicode\n\n:param power_parameters_{param}: The parameter(s) for the power_type.\n Note that this is dynamic as the available parameters depend on\n the selected value of the Machine's power_type. `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode", "op": null}, {"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}, {"restful": false, "name": "accept", "method": "POST", "doc": "Accept a machine's enlistment: not allowed to anonymous users.\n\nAlways returns 401.", "op": "accept"}], "doc": "Anonymous access to Machines.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "path": "/MAAS/api/2.0/machines/"}, "auth": {"name": "MachinesHandler", "params": [], "actions": [{"restful": false, "name": "add_chassis", "method": "POST", "doc": "Add special hardware types.\n\n:param chassis_type: The type of hardware.\n mscm is the type for the Moonshot Chassis Manager.\n msftocs is the type for the Microsoft OCS Chassis Manager.\n powerkvm is the type for Virtual Machines on Power KVM,\n managed by Virsh.\n seamicro15k is the type for the Seamicro 1500 Chassis.\n ucsm is the type for the Cisco UCS Manager.\n virsh is the type for virtual machines managed by Virsh.\n vmware is the type for virtual machines managed by VMware.\n:type chassis_type: unicode\n\n:param hostname: The URL, hostname, or IP address to access the\n chassis.\n:type url: unicode\n\n:param username: The username used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type username: unicode\n\n:param password: The password used to access the chassis. This field\n is required for the seamicro15k, vmware, mscm, msftocs, and ucsm\n chassis types.\n:type password: unicode\n\n:param accept_all: If true, all enlisted machines will be\n commissioned.\n:type accept_all: unicode\n\n:param rack_controller: The system_id of the rack controller to send\n the add chassis command through. If none is specifed MAAS will\n automatically determine the rack controller to use.\n:type rack_controller: unicode\n\n:param domain: The domain that each new machine added should use.\n:type domain: unicode\n\nThe following are optional if you are adding a virsh, vmware, or\npowerkvm chassis:\n\n:param prefix_filter: Filter machines with supplied prefix.\n:type prefix_filter: unicode\n\nThe following are optional if you are adding a seamicro15k chassis:\n\n:param power_control: The power_control to use, either ipmi (default),\n restapi, or restapi2.\n:type power_control: unicode\n\nThe following are optional if you are adding a vmware or msftocs\nchassis.\n\n:param port: The port to use when accessing the chassis.\n:type port: integer\n\nThe following are optioanl if you are adding a vmware chassis:\n\n:param protocol: The protocol to use when accessing the VMware\n chassis (default: https).\n:type protocol: unicode\n\n:return: A string containing the chassis powered on by which rack\n controller.\n\nReturns 404 if no rack controller can be found which has access to the\ngiven URL.\nReturns 403 if the user does not have access to the rack controller.\nReturns 400 if the required parameters were not passed.", "op": "add_chassis"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.", "op": "power_parameters"}, {"restful": false, "name": "accept_all", "method": "POST", "doc": "Accept all declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\n:return: Representations of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.", "op": "accept_all"}, {"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}, {"restful": false, "name": "allocate", "method": "POST", "doc": "Allocate an available machine for deployment.\n\nConstraints parameters can be used to allocate a machine that possesses\ncertain characteristics. All the constraints are optional and when\nmultiple constraints are provided, they are combined using 'AND'\nsemantics.\n\n:param name: Hostname or FQDN of the desired machine. If a FQDN is\n specified, both the domain and the hostname portions must match.\n:type name: unicode\n:param system_id: system_id of the desired machine.\n:type system_id: unicode\n:param arch: Architecture of the returned machine (e.g. 'i386/generic',\n 'amd64', 'armhf/highbank', etc.).\n\n If multiple architectures are specified, the machine to acquire may\n match any of the given architectures. To request multiple\n architectures, this parameter must be repeated in the request with\n each value.\n:type arch: unicode (accepts multiple)\n:param cpu_count: Minimum number of CPUs a returned machine must have.\n\n A machine with additional CPUs may be allocated if there is no\n exact match, or if the 'mem' constraint is not also specified.\n:type cpu_count: positive integer\n:param mem: The minimum amount of memory (expressed in MB) the\n returned machine must have. A machine with additional memory may\n be allocated if there is no exact match, or the 'cpu_count'\n constraint is not also specified.\n:type mem: positive integer\n:param tags: Tags the machine must match in order to be acquired.\n\n If multiple tag names are specified, the machine must be\n tagged with all of them. To request multiple tags, this parameter\n must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param not_tags: Tags the machine must NOT match.\n\n If multiple tag names are specified, the machine must NOT be\n tagged with ANY of them. To request exclusion of multiple tags,\n this parameter must be repeated in the request with each value.\n:type tags: unicode (accepts multiple)\n:param zone: Physical zone name the machine must be located in.\n:type zone: unicode\n:type not_in_zone: List of physical zones from which the machine must\n not be acquired.\n\n If multiple zones are specified, the machine must NOT be\n associated with ANY of them. To request multiple zones to\n exclude, this parameter must be repeated in the request with each\n value.\n:type not_in_zone: unicode (accepts multiple)\n:param subnets: Subnets that must be linked to the machine.\n\n \"Linked to\" means the node must be configured to acquire an address\n in the specified subnet, have a static IP address in the specified\n subnet, or have been observed to DHCP from the specified subnet\n during commissioning time (which implies that it *could* have an\n address on the specified subnet).\n\n Subnets can be specified by one of the following criteria:\n\n - : match the subnet by its 'id' field\n - fabric:: match all subnets in a given fabric.\n - ip:: Match the subnet containing with\n the with the longest-prefix match.\n - name:: Match a subnet with the given name.\n - space:: Match all subnets in a given space.\n - vid:: Match a subnet on a VLAN with the specified\n VID. Valid values range from 0 through 4094 (inclusive). An\n untagged VLAN can be specified by using the value \"0\".\n - vlan:: Match all subnets on the given VLAN.\n\n Note that (as of this writing), the 'fabric', 'space', 'vid', and\n 'vlan' specifiers are only useful for the 'not_spaces' version of\n this constraint, because they will most likely force the query\n to match ALL the subnets in each fabric, space, or VLAN, and thus\n not return any nodes. (This is not a particularly useful behavior,\n so may be changed in the future.)\n\n If multiple subnets are specified, the machine must be associated\n with all of them. To request multiple subnets, this parameter must\n be repeated in the request with each value.\n\n Note that this replaces the leagcy 'networks' constraint in MAAS\n 1.x.\n:type subnets: unicode (accepts multiple)\n:param not_subnets: Subnets that must NOT be linked to the machine.\n\n See the 'subnets' constraint documentation above for more\n information about how each subnet can be specified.\n\n If multiple subnets are specified, the machine must NOT be\n associated with ANY of them. To request multiple subnets to\n exclude, this parameter must be repeated in the request with each\n value. (Or a fabric, space, or VLAN specifier may be used to match\n multiple subnets).\n\n Note that this replaces the leagcy 'not_networks' constraint in\n MAAS 1.x.\n:type not_subnets: unicode (accepts multiple)\n:param storage: A list of storage constraint identifiers, in the form:\n :([,[,...])][,:...]\n:type storage: unicode\n:param interfaces: A labeled constraint map associating constraint\n labels with interface properties that should be matched. Returned\n nodes must have one or more interface matching the specified\n constraints. The labeled constraint map must be in the format:\n ``:=[,=[,...]]``\n\n Each key can be one of the following:\n\n - id: Matches an interface with the specific id\n - fabric: Matches an interface attached to the specified fabric.\n - fabric_class: Matches an interface attached to a fabric\n with the specified class.\n - ip: Matches an interface with the specified IP address\n assigned to it.\n - mode: Matches an interface with the specified mode. (Currently,\n the only supported mode is \"unconfigured\".)\n - name: Matches an interface with the specified name.\n (For example, \"eth0\".)\n - hostname: Matches an interface attached to the node with\n the specified hostname.\n - subnet: Matches an interface attached to the specified subnet.\n - space: Matches an interface attached to the specified space.\n - subnet_cidr: Matches an interface attached to the specified\n subnet CIDR. (For example, \"192.168.0.0/24\".)\n - type: Matches an interface of the specified type. (Valid\n types: \"physical\", \"vlan\", \"bond\", \"bridge\", or \"unknown\".)\n - vlan: Matches an interface on the specified VLAN.\n - vid: Matches an interface on a VLAN with the specified VID.\n - tag: Matches an interface tagged with the specified tag.\n:type interfaces: unicode\n:param fabrics: Set of fabrics that the machine must be associated with\n in order to be acquired.\n\n If multiple fabrics names are specified, the machine can be\n in any of the specified fabrics. To request multiple possible\n fabrics to match, this parameter must be repeated in the request\n with each value.\n:type fabrics: unicode (accepts multiple)\n:param not_fabrics: Fabrics the machine must NOT be associated with in\n order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabrics: unicode (accepts multiple)\n:param fabric_classes: Set of fabric class types whose fabrics the\n machine must be associated with in order to be acquired.\n\n If multiple fabrics class types are specified, the machine can be\n in any matching fabric. To request multiple possible fabrics class\n types to match, this parameter must be repeated in the request\n with each value.\n:type fabric_classes: unicode (accepts multiple)\n:param not_fabric_classes: Fabric class types whose fabrics the machine\n must NOT be associated with in order to be acquired.\n\n If multiple fabrics names are specified, the machine must NOT be\n in ANY of them. To request exclusion of multiple fabrics, this\n parameter must be repeated in the request with each value.\n:type not_fabric_classes: unicode (accepts multiple)\n:param agent_name: An optional agent name to attach to the\n acquired machine.\n:type agent_name: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:param bridge_all: Optionally create a bridge interface for every\n configured interface on the machine. The created bridges will be\n removed once the machine is released.\n (Default: False)\n:type bridge_all: boolean\n:param bridge_stp: Optionally turn spanning tree protocol on or off\n for the bridges created on every configured interface.\n (Default: off)\n:type bridge_stp: boolean\n:param bridge_fd: Optionally adjust the forward delay to time seconds.\n (Default: 15)\n:type bridge_fd: integer\n:param dry_run: Optional boolean to indicate that the machine should\n not actually be acquired (this is for support/troubleshooting, or\n users who want to see which machine would match a constraint,\n without acquiring a machine). Defaults to False.\n:type dry_run: bool\n:param verbose: Optional boolean to indicate that the user would like\n additional verbosity in the constraints_by_type field (each\n constraint will be prefixed by `verbose_`, and contain the full\n data structure that indicates which machine(s) matched).\n:type verbose: bool\n\nReturns 409 if a suitable machine matching the constraints could not be\nfound.", "op": "allocate"}, {"restful": true, "name": "create", "method": "POST", "doc": "Create a new Machine.\n\nAdding a server to MAAS puts it on a path that will wipe its disks\nand re-install its operating system, in the event that it PXE boots.\nIn anonymous enlistment (and when the enlistment is done by a\nnon-admin), the machine is held in the \"New\" state for approval by a\nMAAS admin.\n\nThe minimum data required is:\narchitecture= (e.g. \"i386/generic\")\nmac_addresses= (e.g. \"aa:bb:cc:dd:ee:ff\")\n\n:param architecture: A string containing the architecture type of\n the machine. (For example, \"i386\", or \"amd64\".) To determine the\n supported architectures, use the boot-resources endpoint.\n:type architecture: unicode\n\n:param min_hwe_kernel: A string containing the minimum kernel version\n allowed to be ran on this machine.\n:type min_hwe_kernel: unicode\n\n:param subarchitecture: A string containing the subarchitecture type\n of the machine. (For example, \"generic\" or \"hwe-t\".) To determine\n the supported subarchitectures, use the boot-resources endpoint.\n:type subarchitecture: unicode\n\n:param mac_addresses: One or more MAC addresses for the machine. To\n specify more than one MAC address, the parameter must be specified\n twice. (such as \"machines new mac_addresses=01:02:03:04:05:06\n mac_addresses=02:03:04:05:06:07\")\n:type mac_addresses: unicode\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the machine. If not given the default\n domain is used.\n:type domain: unicode\n\n:param power_type: A power management type, if applicable (e.g.\n \"virsh\", \"ipmi\").\n:type power_type: unicode", "op": null}, {"restful": false, "name": "list_allocated", "method": "GET", "doc": "Fetch Machines that were allocated to the User/oauth token.", "op": "list_allocated"}, {"restful": false, "name": "accept", "method": "POST", "doc": "Accept declared machines into the MAAS.\n\nMachines can be enlisted in the MAAS anonymously or by non-admin users,\nas opposed to by an admin. These machines are held in the New\nstate; a MAAS admin must first verify the authenticity of these\nenlistments, and accept them.\n\nEnlistments can be accepted en masse, by passing multiple machines to\nthis call. Accepting an already accepted machine is not an error, but\naccepting one that is already allocated, broken, etc. is.\n\n:param machines: system_ids of the machines whose enlistment is to be\n accepted. (An empty list is acceptable).\n:return: The system_ids of any machines that have their status changed\n by this call. Thus, machines that were already accepted are\n excluded from the result.\n\nReturns 400 if any of the machines do not exist.\nReturns 403 if the user is not an admin.", "op": "accept"}, {"restful": false, "name": "release", "method": "POST", "doc": "Release multiple machines.\n\nThis places the machines back into the pool, ready to be reallocated.\n\n:param machines: system_ids of the machines which are to be released.\n (An empty list is acceptable).\n:param comment: Optional comment for the event log.\n:type comment: unicode\n:return: The system_ids of any machines that have their status\n changed by this call. Thus, machines that were already released\n are excluded from the result.\n\nReturns 400 if any of the machines cannot be found.\nReturns 403 if the user does not have permission to release any of\nthe machines.\nReturns a 409 if any of the machines could not be released due to their\ncurrent state.", "op": "release"}], "doc": "Manage the collection of all the machines in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/machines/", "path": "/MAAS/api/2.0/machines/"}}, {"name": "DiscoveryHandler", "anon": null, "auth": {"name": "DiscoveryHandler", "params": ["discovery_id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}], "doc": "Read or delete an observed discovery.", "uri": "http://localhost:5240/MAAS/api/2.0/discovery/{discovery_id}/", "path": "/MAAS/api/2.0/discovery/{discovery_id}/"}}, {"name": "NodeResultsHandler", "anon": null, "auth": {"name": "NodeResultsHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "List NodeResult visible to the user, optionally filtered.\n\n:param system_id: An optional list of system ids. Only the\n results related to the nodes with these system ids\n will be returned.\n:type system_id: iterable\n:param name: An optional list of names. Only the results\n with the specified names will be returned.\n:type name: iterable\n:param result_type: An optional result_type. Only the results\n with the specified result_type will be returned.\n:type name: iterable", "op": null}], "doc": "Read the collection of NodeResult in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/installation-results/", "path": "/MAAS/api/2.0/installation-results/"}}, {"name": "FanNetworksHandler", "anon": null, "auth": {"name": "FanNetworksHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a fannetwork.\n\n:param name: Name of the fannetwork.\n:param overlay: Overlay network\n:param underlay: Underlay network\n:param dhcp: confiugre dhcp server for overlay net\n:param host_reserve: number of IP addresses to reserve for host\n:param bridge: override bridge name\n:param off: put this int he config, but disable it.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all fannetworks.", "op": null}], "doc": "Manage Fan Networks.", "uri": "http://localhost:5240/MAAS/api/2.0/fannetworks/", "path": "/MAAS/api/2.0/fannetworks/"}}, {"name": "NotificationsHandler", "anon": null, "auth": {"name": "NotificationsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a notification.\n\nThis is available to admins *only*.\n\n:param message: The message for this notification. May contain basic\n HTML; this will be sanitised before display.\n:param context: Optional JSON context. The root object *must* be an\n object (i.e. a mapping). The values herein can be referenced by\n `message` with Python's \"format\" (not %) codes.\n:param category: Optional category. Choose from: error, warning,\n success, or info. Defaults to info.\n\n:param ident: Optional unique identifier for this notification.\n:param user: Optional user ID this notification is intended for. By\n default it will not be targeted to any individual user.\n:param users: Optional boolean, true to notify all users, defaults to\n false, i.e. not targeted to all users.\n:param admins: Optional boolean, true to notify all admins, defaults to\n false, i.e. not targeted to all admins.\n\nNote: if neither user nor users nor admins is set, the notification\nwill not be seen by anyone.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List notifications relevant to the invoking user.\n\nNotifications that have been dismissed are *not* returned.", "op": null}], "doc": "Manage the collection of all the notifications in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/notifications/", "path": "/MAAS/api/2.0/notifications/"}}, {"name": "DNSResourceRecordHandler", "anon": null, "auth": {"name": "DNSResourceRecordHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read dnsresourcerecord.\n\nReturns 404 if the dnsresourcerecord is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update dnsresourcerecord.\n\n:param rrtype: Resource Type\n:param rrdata: Resource Data (everything to the right of Type.)\n\nReturns 403 if the user does not have permission to update the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete dnsresourcerecord.\n\nReturns 403 if the user does not have permission to delete the\ndnsresourcerecord.\nReturns 404 if the dnsresourcerecord is not found.", "op": null}], "doc": "Manage dnsresourcerecord.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/{id}/", "path": "/MAAS/api/2.0/dnsresourcerecords/{id}/"}}, {"name": "RaidHandler", "anon": null, "auth": {"name": "RaidHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read RAID device on a machine.\n\nReturns 404 if the machine or RAID is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update RAID on a machine.\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param add_block_devices: Block devices to add to the RAID.\n:param remove_block_devices: Block devices to remove from the RAID.\n:param add_spare_devices: Spare block devices to add to the RAID.\n:param remove_spare_devices: Spare block devices to remove\n from the RAID.\n:param add_partitions: Partitions to add to the RAID.\n:param remove_partitions: Partitions to remove from the RAID.\n:param add_spare_partitions: Spare partitions to add to the RAID.\n:param remove_spare_partitions: Spare partitions to remove from the\n RAID.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete RAID on a machine.\n\nReturns 404 if the machine or RAID is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage a specific RAID device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raid/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/raid/{id}/"}}, {"name": "RackControllerHandler", "anon": null, "auth": {"name": "RackControllerHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "import_boot_images", "method": "POST", "doc": "Import the boot images on this rack controller.\n\nReturns 404 if the rack controller is not found.", "op": "import_boot_images"}, {"restful": false, "name": "test", "method": "POST", "doc": "Begin testing process for a node.\n\n:param enable_ssh: Whether to enable SSH for the testing environment\n using the user's SSH key(s).\n:type enable_ssh: bool ('0' for False, '1' for True)\n:param testing_scripts: A comma seperated list of testing script names\n and tags to be run. By default all tests tagged 'commissioning'\n will be run.\n:type testing_scripts: string\n\nA node in the 'ready', 'allocated', 'deployed', 'broken', or any failed\nstate may run tests. If testing is started and successfully passes from\na 'broken', or any failed state besides 'failed commissioning' the node\nwill be returned to a ready state. Otherwise the node will return to\nthe state it was when testing started.\n\nReturns 404 if the node is not found.", "op": "test"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}, {"restful": false, "name": "power_off", "method": "POST", "doc": "Power off a node.\n\n:param stop_mode: An optional power off mode. If 'soft',\n perform a soft power down if the node's power type supports\n it, otherwise perform a hard power off. For all values other\n than 'soft', and by default, perform a hard power off. A\n soft power off generally asks the OS to shutdown the system\n gracefully before powering off, while a hard power off\n occurs immediately without any warning to the OS.\n:type stop_mode: unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to stop the node.", "op": "power_off"}, {"restful": false, "name": "list_boot_images", "method": "GET", "doc": "List all available boot images.\n\nShows all available boot images and lists whether they are in sync with\nthe region.\n\nReturns 404 if the rack controller is not found.", "op": "list_boot_images"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Rack controller.\n\n:param power_type: The new power type for this rack controller. If you\n use the default value, power_parameters will be set to the empty\n string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the rack controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this rack controller should be checked against the\n expected power parameters for the rack controller's power type\n ('true' or 'false'). The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n rack controller.\n:type zone: unicode\n\nReturns 404 if the rack controller is not found.\nReturns 403 if the user does not have permission to update the rack\ncontroller.", "op": null}, {"restful": false, "name": "power_on", "method": "POST", "doc": "Turn on a node.\n\n:param user_data: If present, this blob of user-data to be made\n available to the nodes through the metadata service.\n:type user_data: base64-encoded unicode\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nIdeally we'd have MIME multipart and content-transfer-encoding etc.\ndeal with the encapsulation of binary data, but couldn't make it work\nwith the framework in reasonable time so went for a dumb, manual\nencoding instead.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to start the machine.\nReturns 503 if the start-up attempted to allocate an IP address,\nand there were no IP addresses available on the relevant cluster\ninterface.", "op": "power_on"}, {"restful": false, "name": "abort", "method": "POST", "doc": "Abort a node's current operation.\n\n:param comment: Optional comment for the event log.\n:type comment: unicode\n\nReturns 404 if the node could not be found.\nReturns 403 if the user does not have permission to abort the\ncurrent operation.", "op": "abort"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "query_power_state", "method": "GET", "doc": "Query the power state of a node.\n\nSend a request to the node's power controller which asks it about\nthe node's state. The reply to this could be delayed by up to\n30 seconds while waiting for the power controller to respond.\nUse this method sparingly as it ties up an appserver thread\nwhile waiting.\n\n:param system_id: The node to query.\n:return: a dict whose key is \"state\" with a value of one of\n 'on' or 'off'.\n\nReturns 404 if the node is not found.\nReturns node's power state.", "op": "query_power_state"}], "doc": "Manage an individual rack controller.\n\nThe rack controller is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/{system_id}/", "path": "/MAAS/api/2.0/rackcontrollers/{system_id}/"}}, {"name": "SSHKeyHandler", "anon": null, "auth": {"name": "SSHKeyHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET an SSH key.\n\nReturns 404 if the key does not exist.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE an SSH key.\n\nReturns 404 if the key does not exist.\nReturns 401 if the key does not belong to the calling user.", "op": null}], "doc": "Manage an SSH key.\n\nSSH keys can be retrieved or deleted.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/{id}/", "path": "/MAAS/api/2.0/account/prefs/sshkeys/{id}/"}}, {"name": "VlanHandler", "anon": null, "auth": {"name": "VlanHandler", "params": ["fabric_id", "vid"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update VLAN.\n\n:param name: Name of the VLAN.\n:type name: unicode\n:param description: Description of the VLAN.\n:type description: unicode\n:param vid: VLAN ID of the VLAN.\n:type vid: integer\n:param mtu: The MTU to use on the VLAN.\n:type mtu: integer\n:param dhcp_on: Whether or not DHCP should be managed on the VLAN.\n:type dhcp_on: boolean\n:param primary_rack: The primary rack controller managing the VLAN.\n:type primary_rack: system_id\n:param secondary_rack: The secondary rack controller manging the VLAN.\n:type secondary_rack: system_id\n:param relay_vlan: Only set when this VLAN will be using a DHCP relay\n to forward DHCP requests to another VLAN that MAAS is or will run\n the DHCP server. MAAS will not run the DHCP relay itself, it must\n be configured to proxy reqests to the primary and/or secondary\n rack controller interfaces for the VLAN specified in this field.\n:type relay_vlan: ID of VLAN\n:param space: The space this VLAN should be placed in. Passing in an\n empty string (or the string 'undefined') will cause the VLAN to be\n placed in the 'undefined' space.\n:type space: unicode\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete VLAN on fabric.\n\nReturns 404 if the fabric or VLAN is not found.", "op": null}], "doc": "Manage VLAN on a fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/", "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/{vid}/"}}, {"name": "CommissioningScriptHandler", "anon": null, "auth": {"name": "CommissioningScriptHandler", "params": ["name"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a commissioning script.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a commissioning script.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a commissioning script.", "op": null}], "doc": "Manage a custom commissioning script.\n\nThis functionality is only available to administrators.", "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/{name}", "path": "/MAAS/api/2.0/commissioning-scripts/{name}"}}, {"name": "DNSResourceRecordsHandler", "anon": null, "auth": {"name": "DNSResourceRecordsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a dnsresourcerecord.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param rrtype: resource type to create\n:param rrdata: resource data (everything to the right of\n resource type.)", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all dnsresourcerecords.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.", "op": null}], "doc": "Manage dnsresourcerecords.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresourcerecords/", "path": "/MAAS/api/2.0/dnsresourcerecords/"}}, {"name": "RaidsHandler", "anon": null, "auth": {"name": "RaidsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a RAID\n\n:param name: Name of the RAID.\n:param uuid: UUID of the RAID.\n:param level: RAID level.\n:param block_devices: Block devices to add to the RAID.\n:param spare_devices: Spare block devices to add to the RAID.\n:param partitions: Partitions to add to the RAID.\n:param spare_partitions: Spare partitions to add to the RAID.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all RAID devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage all RAID devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/raids/", "path": "/MAAS/api/2.0/nodes/{system_id}/raids/"}}, {"name": "RackControllersHandler", "anon": {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, "auth": {"name": "RackControllersHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": false, "name": "power_parameters", "method": "GET", "doc": "Retrieve power parameters for multiple machines.\n\n:param id: An optional list of system ids. Only machines with\n matching system ids will be returned.\n:type id: iterable\n\n:return: A dictionary of power parameters, keyed by machine system_id.\n\nRaises 403 if the user is not an admin.", "op": "power_parameters"}, {"restful": false, "name": "import_boot_images", "method": "POST", "doc": "Import the boot images on all rack controllers.", "op": "import_boot_images"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}, {"restful": false, "name": "describe_power_types", "method": "GET", "doc": "Query all of the rack controllers for power information.\n\n:return: a list of dicts that describe the power types in this format.", "op": "describe_power_types"}], "doc": "Manage the collection of all rack controllers in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/rackcontrollers/", "path": "/MAAS/api/2.0/rackcontrollers/"}}, {"name": "AccountHandler", "anon": null, "auth": {"name": "AccountHandler", "params": [], "actions": [{"restful": false, "name": "list_authorisation_tokens", "method": "GET", "doc": "List authorisation tokens available to the currently logged-in user.\n\n:return: list of dictionaries representing each key's name and token.", "op": "list_authorisation_tokens"}, {"restful": false, "name": "update_token_name", "method": "POST", "doc": "Modify the consumer name of an authorisation OAuth token.\n\n:param token: Can be the whole token or only the token key.\n:type token: unicode\n:param name: New name of the token.\n:type name: unicode", "op": "update_token_name"}, {"restful": false, "name": "delete_authorisation_token", "method": "POST", "doc": "Delete an authorisation OAuth token and the related OAuth consumer.\n\n:param token_key: The key of the token to be deleted.\n:type token_key: unicode", "op": "delete_authorisation_token"}, {"restful": false, "name": "create_authorisation_token", "method": "POST", "doc": "Create an authorisation OAuth token and OAuth consumer.\n\n:param name: Optional name of the token that will be generated.\n:type name: unicode\n:return: a json dict with four keys: 'token_key',\n 'token_secret', 'consumer_key' and 'name'(e.g.\n {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34',\n consumer_key: '68543fhj854fg', name: 'MAAS consumer'}).\n:rtype: string (json)", "op": "create_authorisation_token"}], "doc": "Manage the current logged-in user.", "uri": "http://localhost:5240/MAAS/api/2.0/account/", "path": "/MAAS/api/2.0/account/"}}, {"name": "SSHKeysHandler", "anon": null, "auth": {"name": "SSHKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new SSH key to the requesting user's account.\n\nThe request payload should contain the public SSH key data in form\ndata whose name is \"key\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all keys belonging to the requesting user.", "op": null}, {"restful": false, "name": "import", "method": "POST", "doc": "Import the requesting user's SSH keys.\n\nImport SSH keys for a given protocol and authorization ID in\nprotocol:auth_id format.", "op": "import"}], "doc": "Manage the collection of all the SSH keys in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sshkeys/", "path": "/MAAS/api/2.0/account/prefs/sshkeys/"}}, {"name": "VlansHandler", "anon": null, "auth": {"name": "VlansHandler", "params": ["fabric_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a VLAN.\n\n:param name: Name of the VLAN.\n:param description: Description of the VLAN.\n:param vid: VLAN ID of the VLAN.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all VLANs belonging to fabric.\n\nReturns 404 if the fabric is not found.", "op": null}], "doc": "Manage VLANs on a fabric.", "uri": "http://localhost:5240/MAAS/api/2.0/fabrics/{fabric_id}/vlans/", "path": "/MAAS/api/2.0/fabrics/{fabric_id}/vlans/"}}, {"name": "BootResourceHandler", "anon": null, "auth": {"name": "BootResourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot resource.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete boot resource.", "op": null}], "doc": "Manage a boot resource.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/{id}/", "path": "/MAAS/api/2.0/boot-resources/{id}/"}}, {"name": "CommissioningScriptsHandler", "anon": null, "auth": {"name": "CommissioningScriptsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new commissioning script.\n\nEach commissioning script is identified by a unique name.\n\nBy convention the name should consist of a two-digit number, a dash,\nand a brief descriptive identifier consisting only of ASCII\ncharacters. You don't need to follow this convention, but not doing\nso opens you up to risks w.r.t. encoding and ordering. The name must\nnot contain any whitespace, quotes, or apostrophes.\n\nA commissioning machine will run each of the scripts in lexicographical\norder. There are no promises about how non-ASCII characters are\nsorted, or even how upper-case letters are sorted relative to\nlower-case letters. So where ordering matters, use unique numbers.\n\nScripts built into MAAS will have names starting with \"00-maas\" or\n\"99-maas\" to ensure that they run first or last, respectively.\n\nUsually a commissioning script will be just that, a script. Ideally a\nscript should be ASCII text to avoid any confusion over encoding. But\nin some cases a commissioning script might consist of a binary tool\nprovided by a hardware vendor. Either way, the script gets passed to\nthe commissioning machine in the exact form in which it was uploaded.\n\n:param name: Unique identifying name for the script. Names should\n follow the pattern of \"25-burn-in-hard-disk\" (all ASCII, and with\n numbers greater than zero, and generally no \"weird\" characters).\n:param content: A script file, to be uploaded in binary form. Note:\n this is not a normal parameter, but a file upload. Its filename\n is ignored; MAAS will know it by the name you pass to the request.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List commissioning scripts.", "op": null}], "doc": "Manage custom commissioning scripts.\n\nThis functionality is only available to administrators.", "uri": "http://localhost:5240/MAAS/api/2.0/commissioning-scripts/", "path": "/MAAS/api/2.0/commissioning-scripts/"}}, {"name": "DiscoveriesHandler", "anon": null, "auth": {"name": "DiscoveriesHandler", "params": [], "actions": [{"restful": false, "name": "by_unknown_ip", "method": "GET", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with the IP address of the\ndiscovery, or has been observed using it after it was assigned by\na MAAS-managed DHCP server.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_ip"}, {"restful": false, "name": "scan", "method": "POST", "doc": "Immediately run a neighbour discovery scan on all rack networks.\n\nThis command causes each connected rack controller to execute the\n'maas-rack scan-network' command, which will scan all CIDRs configured\non the rack controller using 'nmap' (if it is installed) or 'ping'.\n\nNetwork discovery must not be set to 'disabled' for this command to be\nuseful.\n\nScanning will be started in the background, and could take a long time\non rack controllers that do not have 'nmap' installed and are connected\nto large networks.\n\nIf the call is a success, this method will return a dictionary of\nresults as follows:\n\nresult: A human-readable string summarizing the results.\nscan_attempted_on: A list of rack 'system_id' values where a scan\nwas attempted. (That is, an RPC connection was successful and a\nsubsequent call was intended.)\n\nfailed_to_connect_to: A list of rack 'system_id' values where the RPC\nconnection failed.\n\nscan_started_on: A list of rack 'system_id' values where a scan was\nsuccessfully started.\n\nscan_failed_on: A list of rack 'system_id' values where\na scan was attempted, but failed because a scan was already in\nprogress.\n\nrpc_call_timed_out_on: A list of rack 'system_id' values where the\nRPC connection was made, but the call timed out before a ten second\ntimeout elapsed.\n\n:param cidr: The subnet CIDR(s) to scan (can be specified multiple\n times). If not specified, defaults to all networks.\n:param force: If True, will force the scan, even if all networks are\n specified. (This may not be the best idea, depending on acceptable\n use agreements, and the politics of the organization that owns the\n network.) Default: False.\n:param always_use_ping: If True, will force the scan to use 'ping' even\n if 'nmap' is installed. Default: False.\n:param slow: If True, and 'nmap' is being used, will limit the scan\n to nine packets per second. If the scanner is 'ping', this option\n has no effect. Default: False.\n:param threads: The number of threads to use during scanning. If 'nmap'\n is the scanner, the default is one thread per 'nmap' process. If\n 'ping' is the scanner, the default is four threads per CPU.", "op": "scan"}, {"restful": false, "name": "by_unknown_mac", "method": "GET", "doc": "Lists all discovered devices which have an unknown IP address.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere an interface known to MAAS is configured with MAC address of the\ndiscovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_mac"}, {"restful": false, "name": "by_unknown_ip_and_mac", "method": "GET", "doc": "Lists all discovered devices which are completely unknown to MAAS.\n\nFilters the list of discovered devices by excluding any discoveries\nwhere a known MAAS node is configured with either the MAC address or\nthe IP address of the discovery.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": "by_unknown_ip_and_mac"}, {"restful": false, "name": "clear", "method": "POST", "doc": "Deletes all discovered neighbours and/or mDNS entries.\n\n:param mdns: if True, deletes all mDNS entries.\n:param neighbours: if True, deletes all neighbour entries.\n:param all: if True, deletes all discovery data.", "op": "clear"}, {"restful": true, "name": "read", "method": "GET", "doc": "Lists all the devices MAAS has discovered.\n\nDiscoveries are listed in the order they were last observed on the\nnetwork (most recent first).", "op": null}], "doc": "Query observed discoveries.", "uri": "http://localhost:5240/MAAS/api/2.0/discovery/", "path": "/MAAS/api/2.0/discovery/"}}, {"name": "DNSResourceHandler", "anon": null, "auth": {"name": "DNSResourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read dnsresource.\n\nReturns 404 if the dnsresource is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource.\n:param ip_address: Address to assign to the dnsresource.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the dnsresource is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete dnsresource.\n\nReturns 403 if the user does not have permission to delete the\ndnsresource.\nReturns 404 if the dnsresource is not found.", "op": null}], "doc": "Manage dnsresource.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/{id}/", "path": "/MAAS/api/2.0/dnsresources/{id}/"}}, {"name": "BcacheHandler", "anon": null, "auth": {"name": "BcacheHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read bcache device on a machine.\n\nReturns 404 if the machine or bcache is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Delete bcache on a machine.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set to replace current one.\n:param backing_device: Backing block device to replace current one.\n:param backing_partition: Backing partition to replace current one.\n:param cache_mode: Cache mode (writeback, writethrough, writearound).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine or the bcache is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete bcache on a machine.\n\nReturns 404 if the machine or bcache is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage bcache device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache/{id}/"}}, {"name": "RegionControllerHandler", "anon": null, "auth": {"name": "RegionControllerHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Region controller.\n\n:param power_type: The new power type for this region controller. If\n you use the default value, power_parameters will be set to the\n empty string.\n Available to admin users.\n See the `Power types`_ section for a list of the available power\n types.\n:type power_type: unicode\n\n:param power_parameters_{param1}: The new value for the 'param1'\n power parameter. Note that this is dynamic as the available\n parameters depend on the selected value of the region controller's\n power_type. Available to admin users. See the `Power types`_\n section for a list of the available power parameters for each\n power type.\n:type power_parameters_{param1}: unicode\n\n:param power_parameters_skip_check: Whether or not the new power\n parameters for this region controller should be checked against the\n expected power parameters for the region controller's power type\n ('true' or 'false').\n The default is 'false'.\n:type power_parameters_skip_check: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n region controller.\n:type zone: unicode\n\nReturns 404 if the region controller is not found.\nReturns 403 if the user does not have permission to update the region\ncontroller.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Node.\n\nReturns 404 if the node is not found.\nReturns 403 if the user does not have permission to delete the node.\nReturns 204 if the node is successfully deleted.", "op": null}], "doc": "Manage an individual region controller.\n\nThe region controller is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/{system_id}/", "path": "/MAAS/api/2.0/regioncontrollers/{system_id}/"}}, {"name": "EventsHandler", "anon": null, "auth": {"name": "EventsHandler", "params": [], "actions": [{"restful": false, "name": "query", "method": "GET", "doc": "List Node events, optionally filtered by various criteria via\nURL query parameters.\n\n:param hostname: An optional hostname. Only events relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to get events relating to more than one node.\n:param mac_address: An optional list of MAC addresses. Only\n nodes with matching MAC addresses will be returned.\n:param id: An optional list of system ids. Only nodes with\n matching system ids will be returned.\n:param zone: An optional name for a physical zone. Only nodes in the\n zone will be returned.\n:param agent_name: An optional agent name. Only nodes with\n matching agent names will be returned.\n:param level: Desired minimum log level of returned events. Returns\n this level of events and greater. Choose from: CRITICAL, DEBUG, ERROR, INFO, WARNING.\n The default is INFO.\n:param limit: Optional number of events to return. Default 100.\n Maximum: 1000.\n:param before: Optional event id. Defines where to start returning\n older events.\n:param after: Optional event id. Defines where to start returning\n newer events.", "op": "query"}], "doc": "Retrieve filtered node events.\n\nA specific Node's events is identified by specifying one or more\nids, hostnames, or mac addresses as a list.", "uri": "http://localhost:5240/MAAS/api/2.0/events/", "path": "/MAAS/api/2.0/events/"}}, {"name": "MaasHandler", "anon": null, "auth": {"name": "MaasHandler", "params": [], "actions": [{"restful": false, "name": "set_config", "method": "POST", "doc": "Set a config value.\n\n:param name: The name of the config item to be set.\n:param value: The value of the config item to be set.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)", "op": "set_config"}, {"restful": false, "name": "get_config", "method": "GET", "doc": "Get a config value.\n\n:param name: The name of the config item to be retrieved.\n\nAvailable configuration items:\n\n:active_discovery_interval: Active subnet mapping interval. When enabled, each rack will scan subnets enabled for active mapping. This helps ensure discovery information is accurate and complete.\n:boot_images_auto_import: Automatically import/refresh the boot images every 60 minutes.\n:commissioning_distro_series: Default Ubuntu release used for commissioning.\n:completed_intro: Marks if the initial intro has been completed..\n:curtin_verbose: Run the fast-path installer with higher verbosity. This provides more detail in the installation logs..\n:default_distro_series: Default OS release used for deployment.\n:default_dns_ttl: Default Time-To-Live for the DNS.. If no TTL value is specified at a more specific point this is how long DNS responses are valid, in seconds.\n:default_min_hwe_kernel: Default Minimum Kernel Version. The default minimum kernel version used on all new and commissioned nodes.\n:default_osystem: Default operating system used for deployment.\n:default_storage_layout: Default storage layout. Storage layout that is applied to a node when it is commissioned. Available choices are: 'bcache' (Bcache layout), 'flat' (Flat layout), 'lvm' (LVM layout).\n:disk_erase_with_quick_erase: Use quick erase by default when erasing disks.. This is not a secure erase; it wipes only the beginning and end of each disk.\n:disk_erase_with_secure_erase: Use secure erase by default when erasing disks.. Will only be used on devices that support secure erase. Other devices will fall back to full wipe or quick erase depending on the selected options.\n:dnssec_validation: Enable DNSSEC validation of upstream zones. Only used when MAAS is running its own DNS server. This value is used as the value of 'dnssec_validation' in the DNS server config.\n:enable_analytics: Enable MAAS UI usage of Google Analytics. This helps the developers of MAAS to identify usage statistics to further development..\n:enable_disk_erasing_on_release: Erase nodes' disks prior to releasing.. Forces users to always erase disks when releasing.\n:enable_http_proxy: Enable the use of an APT and HTTP/HTTPS proxy. Provision nodes to use the built-in HTTP proxy (or user specified proxy) for APT. MAAS also uses the proxy for downloading boot images.\n:enable_third_party_drivers: Enable the installation of proprietary drivers (i.e. HPVSA).\n:http_proxy: Proxy for APT and HTTP/HTTPS. This will be passed onto provisioned nodes to use as a proxy for APT traffic. MAAS also uses the proxy for downloading boot images. If no URL is provided, the built-in MAAS proxy will be used.\n:kernel_opts: Boot parameters to pass to the kernel by default.\n:maas_name: MAAS name.\n:max_node_commissioning_results: The maximum number of commissioning results runs which are stored..\n:max_node_installation_results: The maximum number of installation result runs which are stored..\n:max_node_testing_results: The maximum number of testing results runs which are stored..\n:network_discovery: . When enabled, MAAS will use passive techniques (such as listening to ARP requests and mDNS advertisements) to observe networks attached to rack controllers. Active subnet mapping will also be available to be enabled on the configured subnets.\n:ntp_external_only: Use external NTP servers only. Configure all region controller hosts, rack controller hosts, and subsequently deployed machines to refer directly to the configured external NTP servers. Otherwise only region controller hosts will be configured to use those external NTP servers, rack contoller hosts will in turn refer to the regions' NTP servers, and deployed machines will refer to the racks' NTP servers.\n:ntp_servers: Addresses of NTP servers. NTP servers, specified as IP addresses or hostnames delimited by commas and/or spaces, to be used as time references for MAAS itself, the machines MAAS deploys, and devices that make use of MAAS's DHCP services.\n:upstream_dns: Upstream DNS used to resolve domains not managed by this MAAS (space-separated IP addresses). Only used when MAAS is running its own DNS server. This value is used as the value of 'forwarders' in the DNS server config.\n:windows_kms_host: Windows KMS activation host. FQDN or IP address of the host that provides the KMS Windows activation service. (Only needed for Windows deployments using KMS activation.)", "op": "get_config"}], "doc": "Manage the MAAS server.", "uri": "http://localhost:5240/MAAS/api/2.0/maas/", "path": "/MAAS/api/2.0/maas/"}}, {"name": "SSLKeyHandler", "anon": null, "auth": {"name": "SSLKeyHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET an SSL key.\n\nReturns 404 if the key with `id` is not found.\nReturns 401 if the key does not belong to the requesting user.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE an SSL key.\n\nReturns 401 if the key does not belong to the requesting user.\nReturns 204 if the key is successfully deleted.", "op": null}], "doc": "Manage an SSL key.\n\nSSL keys can be retrieved or deleted.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/{id}/", "path": "/MAAS/api/2.0/account/prefs/sslkeys/{id}/"}}, {"name": "SpaceHandler", "anon": null, "auth": {"name": "SpaceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read space.\n\nReturns 404 if the space is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update space.\n\n:param name: Name of the space.\n:param description: Description of the space.\n\nReturns 404 if the space is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete space.\n\nReturns 404 if the space is not found.", "op": null}], "doc": "Manage space.", "uri": "http://localhost:5240/MAAS/api/2.0/spaces/{id}/", "path": "/MAAS/api/2.0/spaces/{id}/"}}, {"name": "NetworkHandler", "anon": null, "auth": {"name": "NetworkHandler", "params": ["name"], "actions": [{"restful": false, "name": "disconnect_macs", "method": "POST", "doc": "Disconnect the given MAC addresses from this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": "disconnect_macs"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": null}, {"restful": false, "name": "connect_macs", "method": "POST", "doc": "Connect the given MAC addresses to this network.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.", "op": "connect_macs"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read network definition.", "op": null}, {"restful": false, "name": "list_connected_macs", "method": "GET", "doc": "Returns the list of MAC addresses connected to this network.\n\nOnly MAC addresses for nodes visible to the requesting user are\nreturned.", "op": "list_connected_macs"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update network definition.\n\nThis endpoint is no longer available. Use the 'subnet' endpoint\ninstead.\n\n:param name: A simple name for the network, to make it easier to\n refer to. Must consist only of letters, digits, dashes, and\n underscores.\n:param ip: Base IP address for the network, e.g. `10.1.0.0`. The host\n bits will be zeroed.\n:param netmask: Subnet mask to indicate which parts of an IP address\n are part of the network address. For example, `255.255.255.0`.\n:param vlan_tag: Optional VLAN tag: a number between 1 and 0xffe (4094)\n inclusive, or zero for an untagged network.\n:param description: Detailed description of the network for the benefit\n of users and administrators.", "op": null}], "doc": "Manage a network.\n\nThis endpoint is deprecated. Use the new 'subnet' endpoint instead.", "uri": "http://localhost:5240/MAAS/api/2.0/networks/{name}/", "path": "/MAAS/api/2.0/networks/{name}/"}}, {"name": "BootResourcesHandler", "anon": null, "auth": {"name": "BootResourcesHandler", "params": [], "actions": [{"restful": false, "name": "stop_import", "method": "POST", "doc": "Stop import of boot resources.", "op": "stop_import"}, {"restful": true, "name": "create", "method": "POST", "doc": "Uploads a new boot resource.\n\n:param name: Name of the boot resource.\n:param title: Title for the boot resource.\n:param architecture: Architecture the boot resource supports.\n:param filetype: Filetype for uploaded content. (Default: tgz)\n:param content: Image content. Note: this is not a normal parameter,\n but a file upload.", "op": null}, {"restful": false, "name": "is_importing", "method": "GET", "doc": "Return import status.", "op": "is_importing"}, {"restful": true, "name": "read", "method": "GET", "doc": "List all boot resources.\n\n:param type: Type of boot resources to list. Default: all", "op": null}, {"restful": false, "name": "import", "method": "POST", "doc": "Import the boot resources.", "op": "import"}], "doc": "Manage the boot resources.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-resources/", "path": "/MAAS/api/2.0/boot-resources/"}}, {"name": "DNSResourcesHandler", "anon": null, "auth": {"name": "DNSResourcesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a dnsresource.\n\n:param fqdn: Hostname (with domain) for the dnsresource. Either fqdn\n or (name, domain) must be specified. Fqdn is ignored if either\n name or domain is given.\n:param name: Hostname (without domain)\n:param domain: Domain (name or id)\n:param address_ttl: Default ttl for entries in this zone.\n:param ip_addresses: (optional) Address (ip or id) to assign to the\n dnsresource.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all resources for the specified criteria.\n\n:param domain: restrict the listing to entries for the domain.\n:param name: restrict the listing to entries of the given name.\n:param rrtype: restrict the listing to entries which have\n records of the given rrtype.", "op": null}], "doc": "Manage dnsresources.", "uri": "http://localhost:5240/MAAS/api/2.0/dnsresources/", "path": "/MAAS/api/2.0/dnsresources/"}}, {"name": "BootSourceHandler", "anon": null, "auth": {"name": "BootSourceHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot source.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for this\n BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded data.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific boot source.", "op": null}], "doc": "Manage a boot source.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{id}/", "path": "/MAAS/api/2.0/boot-sources/{id}/"}}, {"name": "BcachesHandler", "anon": null, "auth": {"name": "BcachesHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a Bcache.\n\n:param name: Name of the Bcache.\n:param uuid: UUID of the Bcache.\n:param cache_set: Cache set.\n:param backing_device: Backing block device.\n:param backing_partition: Backing partition.\n:param cache_mode: Cache mode (WRITEBACK, WRITETHROUGH, WRITEAROUND).\n\nSpecifying both a device and a partition for a given role (cache or\nbacking) is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all bcache devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage bcache devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcaches/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcaches/"}}, {"name": "RegionControllersHandler", "anon": {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, "auth": {"name": "RegionControllersHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all region controllers in MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/regioncontrollers/", "path": "/MAAS/api/2.0/regioncontrollers/"}}, {"name": "FilesHandler", "anon": {"name": "AnonFilesHandler", "params": [], "actions": [{"restful": false, "name": "get_by_key", "method": "GET", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.", "op": "get_by_key"}], "doc": "Anonymous file operations.\n\nThis is needed for Juju. The story goes something like this:\n\n- The Juju provider will upload a file using an \"unguessable\" name.\n\n- The name of this file (or its URL) will be shared with all the agents in\n the environment. They cannot modify the file, but they can access it\n without credentials.", "uri": "http://localhost:5240/MAAS/api/2.0/files/", "path": "/MAAS/api/2.0/files/"}, "auth": {"name": "FilesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new file to the file storage.\n\n:param filename: The file name to use in the storage.\n:type filename: string\n:param file: Actual file data with content type\n application/octet-stream\n\nReturns 400 if any of these conditions apply:\n - The filename is missing from the parameters\n - The file data is missing\n - More than one file is supplied", "op": null}, {"restful": false, "name": "get", "method": "GET", "doc": "Get a named file from the file storage.\n\n:param filename: The exact name of the file you want to get.\n:type filename: string\n:return: The file is returned in the response content.", "op": "get"}, {"restful": true, "name": "read", "method": "GET", "doc": "List the files from the file storage.\n\nThe returned files are ordered by file name and the content is\nexcluded.\n\n:param prefix: Optional prefix used to filter out the returned files.\n:type prefix: string", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a FileStorage object.\n\n:param filename: The filename of the object to be deleted.\n:type filename: unicode", "op": null}, {"restful": false, "name": "get_by_key", "method": "GET", "doc": "Get a file from the file storage using its key.\n\n:param key: The exact key of the file you want to get.\n:type key: string\n:return: The file is returned in the response content.", "op": "get_by_key"}], "doc": "Manage the collection of all the files in this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/files/", "path": "/MAAS/api/2.0/files/"}}, {"name": "SSLKeysHandler", "anon": null, "auth": {"name": "SSLKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Add a new SSL key to the requesting user's account.\n\nThe request payload should contain the SSL key data in form\ndata whose name is \"key\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all keys belonging to the requesting user.", "op": null}], "doc": "Operations on multiple keys.", "uri": "http://localhost:5240/MAAS/api/2.0/account/prefs/sslkeys/", "path": "/MAAS/api/2.0/account/prefs/sslkeys/"}}, {"name": "SpacesHandler", "anon": null, "auth": {"name": "SpacesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a space.\n\n:param name: Name of the space.\n:param description: Description of the space.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all spaces.", "op": null}], "doc": "Manage spaces.", "uri": "http://localhost:5240/MAAS/api/2.0/spaces/", "path": "/MAAS/api/2.0/spaces/"}}, {"name": "BootSourcesHandler", "anon": null, "auth": {"name": "BootSourcesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new boot source.\n\n:param url: The URL of the BootSource.\n:param keyring_filename: The path to the keyring file for\n this BootSource.\n:param keyring_data: The GPG keyring for this BootSource,\n base64-encoded.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List boot sources.\n\nGet a listing of boot sources.", "op": null}], "doc": "Manage the collection of boot sources.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/", "path": "/MAAS/api/2.0/boot-sources/"}}, {"name": "LicenseKeysHandler", "anon": null, "auth": {"name": "LicenseKeysHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Define a license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List license keys.", "op": null}], "doc": "Manage the license keys.", "uri": "http://localhost:5240/MAAS/api/2.0/license-keys/", "path": "/MAAS/api/2.0/license-keys/"}}, {"name": "DomainHandler", "anon": null, "auth": {"name": "DomainHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read domain.\n\nReturns 404 if the domain is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update domain.\n\n:param name: Name of the domain.\n:param authoritative: True if we are authoritative for this domain.\n:param ttl: The default TTL for this domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete domain.\n\nReturns 403 if the user does not have permission to update the\ndnsresource.\nReturns 404 if the domain is not found.", "op": null}], "doc": "Manage domain.", "uri": "http://localhost:5240/MAAS/api/2.0/domains/{id}/", "path": "/MAAS/api/2.0/domains/{id}/"}}, {"name": "BcacheCacheSetHandler", "anon": null, "auth": {"name": "BcacheCacheSetHandler", "params": ["system_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read bcache cache set on a machine.\n\nReturns 404 if the machine or cache set is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Delete bcache on a machine.\n\n:param cache_device: Cache block device to replace current one.\n:param cache_partition: Cache partition to replace current one.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine or the cache set is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete cache set on a machine.\n\nReturns 400 if the cache set is in use.\nReturns 404 if the machine or cache set is not found.\nReturns 409 if the machine is not Ready.", "op": null}], "doc": "Manage bcache cache set on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-set/{id}/"}}, {"name": "DeviceHandler", "anon": null, "auth": {"name": "DeviceHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "power_parameters", "method": "GET", "doc": "Obtain power parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the power parameters, if any, configured for a\nnode. For some types of power control this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the node is not found.", "op": "power_parameters"}, {"restful": false, "name": "set_owner_data", "method": "POST", "doc": "Set key/value data for the current owner.\n\nPass any key/value data to this method to add, modify, or remove. A key\nis removed when the value for that key is set to an empty string.\n\nThis operation will not remove any previous keys unless explicitly\npassed with an empty string. All owner data is removed when the machine\nis no longer allocated to a user.\n\nReturns 404 if the machine is not found.\nReturns 403 if the user does not have permission.", "op": "set_owner_data"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Device.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to delete the device.\nReturns 204 if the device is successfully deleted.", "op": null}, {"restful": false, "name": "restore_default_configuration", "method": "POST", "doc": "Reset a device's configuration to its initial state.\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to reset the device.", "op": "restore_default_configuration"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific device.\n\n:param hostname: The new hostname for this device.\n:type hostname: unicode\n\n:param domain: The domain for this device.\n:type domain: unicode\n\n:param parent: Optional system_id to indicate this device's parent.\n If the parent is already set and this parameter is omitted,\n the parent will be unchanged.\n:type parent: unicode\n\n:param zone: Name of a valid physical zone in which to place this\n node.\n:type zone: unicode\n\nReturns 404 if the device is not found.\nReturns 403 if the user does not have permission to update the device.", "op": null}, {"restful": false, "name": "restore_networking_configuration", "method": "POST", "doc": "Reset a device's network options.\n\nReturns 404 if the device is not found\nReturns 403 if the user does not have permission to reset the device.", "op": "restore_networking_configuration"}, {"restful": false, "name": "details", "method": "GET", "doc": "Obtain various system details.\n\nFor example, LLDP and ``lshw`` XML dumps.\n\nReturns a ``{detail_type: xml, ...}`` map, where\n``detail_type`` is something like \"lldp\" or \"lshw\".\n\nNote that this is returned as BSON and not JSON. This is for\nefficiency, but mainly because JSON can't do binary content\nwithout applying additional encoding like base-64.\n\nReturns 404 if the node is not found.", "op": "details"}, {"restful": true, "name": "read", "method": "GET", "doc": "Read a specific Node.\n\nReturns 404 if the node is not found.", "op": null}], "doc": "Manage an individual device.\n\nThe device is identified by its system_id.", "uri": "http://localhost:5240/MAAS/api/2.0/devices/{system_id}/", "path": "/MAAS/api/2.0/devices/{system_id}/"}}, {"name": "FileHandler", "anon": null, "auth": {"name": "FileHandler", "params": ["filename"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET a FileStorage object as a json object.\n\nThe 'content' of the file is base64-encoded.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a FileStorage object.", "op": null}], "doc": "Manage a FileStorage object.\n\nThe file is identified by its filename and owner.", "uri": "http://localhost:5240/MAAS/api/2.0/files/{filename}/", "path": "/MAAS/api/2.0/files/{filename}/"}}, {"name": "UserHandler", "anon": null, "auth": {"name": "UserHandler", "params": ["username"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Deletes a user", "op": null}], "doc": "Manage a user account.", "uri": "http://localhost:5240/MAAS/api/2.0/users/{username}/", "path": "/MAAS/api/2.0/users/{username}/"}}, {"name": "SubnetHandler", "anon": null, "auth": {"name": "SubnetHandler", "params": ["id"], "actions": [{"restful": false, "name": "ip_addresses", "method": "GET", "doc": "Returns a summary of IP addresses assigned to this subnet.\n\nOptional parameters\n-------------------\n\nwith_username\n If False, suppresses the display of usernames associated with each\n address. (Default: True)\n\nwith_node_summary\n If False, suppresses the display of any node associated with each\n address. (Default: True)", "op": "ip_addresses"}, {"restful": false, "name": "reserved_ip_ranges", "method": "GET", "doc": "Lists IP ranges currently reserved in the subnet.\n\nReturns 404 if the subnet is not found.", "op": "reserved_ip_ranges"}, {"restful": false, "name": "unreserved_ip_ranges", "method": "GET", "doc": "Lists IP ranges currently unreserved in the subnet.\n\nReturns 404 if the subnet is not found.", "op": "unreserved_ip_ranges"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete subnet.\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read subnet.\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update the specified subnet.\n\nPlease see the documentation for the 'create' operation for detailed\ndescriptions of each parameter.\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to.\n\nspace\n Space this subnet is in.\n\ncidr\n The network CIDR for this subnet.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n\nallow_proxy\n Configure maas-proxy to allow requests from this subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n If False, MAAS should not manage this subnet. (Default: True)\n\nReturns 404 if the subnet is not found.", "op": null}, {"restful": false, "name": "statistics", "method": "GET", "doc": "Returns statistics for the specified subnet, including:\n\nnum_available: the number of available IP addresses\nlargest_available: the largest number of contiguous free IP addresses\nnum_unavailable: the number of unavailable IP addresses\ntotal_addresses: the sum of the available plus unavailable addresses\nusage: the (floating point) usage percentage of this subnet\nusage_string: the (formatted unicode) usage percentage of this subnet\nranges: the specific IP ranges present in ths subnet (if specified)\n\nOptional parameters\n-------------------\n\ninclude_ranges\n If True, includes detailed information\n about the usage of this range.\n\ninclude_suggestions\n If True, includes the suggested gateway and dynamic range for this\n subnet, if it were to be configured.\n\nReturns 404 if the subnet is not found.", "op": "statistics"}], "doc": "Manage subnet.", "uri": "http://localhost:5240/MAAS/api/2.0/subnets/{id}/", "path": "/MAAS/api/2.0/subnets/{id}/"}}, {"name": "BootSourceSelectionHandler", "anon": null, "auth": {"name": "BootSourceSelectionHandler", "params": ["boot_source_id", "id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read a boot source selection.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The list of architectures for which to import resources.\n:param subarches: The list of subarchitectures for which to import\n resources.\n:param labels: The list of labels for which to import resources.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific boot source.", "op": null}], "doc": "Manage a boot source selection.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/", "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/{id}/"}}, {"name": "DomainsHandler", "anon": null, "auth": {"name": "DomainsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a domain.\n\n:param name: Name of the domain.\n:param authoritative: Class type of the domain.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all domains.", "op": null}, {"restful": false, "name": "set_serial", "method": "POST", "doc": "Set the SOA serial number (for all DNS zones.)\n\n:param serial: serial number to use next.", "op": "set_serial"}], "doc": "Manage domains.", "uri": "http://localhost:5240/MAAS/api/2.0/domains/", "path": "/MAAS/api/2.0/domains/"}}, {"name": "BcacheCacheSetsHandler", "anon": null, "auth": {"name": "BcacheCacheSetsHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Creates a Bcache Cache Set.\n\n:param cache_device: Cache block device.\n:param cache_partition: Cache partition.\n\nSpecifying both a cache_device and a cache_partition is not allowed.\n\nReturns 404 if the machine is not found.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all bcache cache sets belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage bcache cache sets on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/", "path": "/MAAS/api/2.0/nodes/{system_id}/bcache-cache-sets/"}}, {"name": "DevicesHandler", "anon": {"name": "AnonNodesHandler", "params": [], "actions": [{"restful": false, "name": "is_registered", "method": "GET", "doc": "Returns whether or not the given MAC address is registered within\nthis MAAS (and attached to a non-retired node).\n\n:param mac_address: The mac address to be checked.\n:type mac_address: unicode\n:return: 'true' or 'false'.\n:rtype: unicode\n\nReturns 400 if any mandatory parameters are missing.", "op": "is_registered"}], "doc": "Anonymous access to Nodes.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/", "path": "/MAAS/api/2.0/nodes/"}, "auth": {"name": "DevicesHandler", "params": [], "actions": [{"restful": false, "name": "set_zone", "method": "POST", "doc": "Assign multiple nodes to a physical zone at once.\n\n:param zone: Zone name. If omitted, the zone is \"none\" and the nodes\n will be taken out of their physical zones.\n:param nodes: system_ids of the nodes whose zones are to be set.\n (An empty list is acceptable).\n\nRaises 403 if the user is not an admin.", "op": "set_zone"}, {"restful": true, "name": "create", "method": "POST", "doc": "Create a new device.\n\n:param hostname: A hostname. If not given, one will be generated.\n:type hostname: unicode\n\n:param domain: The domain of the device. If not given the default\n domain is used.\n:type domain: unicode\n\n:param mac_addresses: One or more MAC addresses for the device.\n:type mac_addresses: unicode\n\n:param parent: The system id of the parent. Optional.\n:type parent: unicode", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List Nodes visible to the user, optionally filtered by criteria.\n\nNodes are sorted by id (i.e. most recent last) and grouped by type.\n\n:param hostname: An optional hostname. Only nodes relating to the node\n with the matching hostname will be returned. This can be specified\n multiple times to see multiple nodes.\n:type hostname: unicode\n\n:param mac_address: An optional MAC address. Only nodes relating to the\n node owning the specified MAC address will be returned. This can be\n specified multiple times to see multiple nodes.\n:type mac_address: unicode\n\n:param id: An optional list of system ids. Only nodes relating to the\n nodes with matching system ids will be returned.\n:type id: unicode\n\n:param domain: An optional name for a dns domain. Only nodes relating\n to the nodes in the domain will be returned.\n:type domain: unicode\n\n:param zone: An optional name for a physical zone. Only nodes relating\n to the nodes in the zone will be returned.\n:type zone: unicode\n\n:param agent_name: An optional agent name. Only nodes relating to the\n nodes with matching agent names will be returned.\n:type agent_name: unicode", "op": null}], "doc": "Manage the collection of all the devices in the MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/devices/", "path": "/MAAS/api/2.0/devices/"}}, {"name": "UsersHandler", "anon": null, "auth": {"name": "UsersHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a MAAS user account.\n\nThis is not safe: the password is sent in plaintext. Avoid it for\nproduction, unless you are confident that you can prevent eavesdroppers\nfrom observing the request.\n\n:param username: Identifier-style username for the new user.\n:type username: unicode\n:param email: Email address for the new user.\n:type email: unicode\n:param password: Password for the new user.\n:type password: unicode\n:param is_superuser: Whether the new user is to be an administrator.\n:type is_superuser: bool ('0' for False, '1' for True)\n\nReturns 400 if any mandatory parameters are missing.", "op": null}, {"restful": false, "name": "whoami", "method": "GET", "doc": "Returns the currently logged in user.", "op": "whoami"}, {"restful": true, "name": "read", "method": "GET", "doc": "List users.", "op": null}], "doc": "Manage the user accounts of this MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/users/", "path": "/MAAS/api/2.0/users/"}}, {"name": "BootSourceSelectionsHandler", "anon": null, "auth": {"name": "BootSourceSelectionsHandler", "params": ["boot_source_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new boot source selection.\n\n:param os: The OS (e.g. ubuntu, centos) for which to import resources.\n:param release: The release for which to import resources.\n:param arches: The architecture list for which to import resources.\n:param subarches: The subarchitecture list for which to import\n resources.\n:param labels: The label lists for which to import resources.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List boot source selections.\n\nGet a listing of a boot source's selections.", "op": null}], "doc": "Manage the collection of boot source selections.", "uri": "http://localhost:5240/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/", "path": "/MAAS/api/2.0/boot-sources/{boot_source_id}/selections/"}}, {"name": "SubnetsHandler", "anon": null, "auth": {"name": "SubnetsHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a subnet.\n\nRequired parameters\n-------------------\n\ncidr\n The network CIDR for this subnet.\n\n\nOptional parameters\n-------------------\n\nname\n Name of the subnet.\n\ndescription\n Description of the subnet.\n\nvlan\n VLAN this subnet belongs to. Defaults to the default VLAN for the\n provided fabric or defaults to the default VLAN in the default fabric\n (if unspecified).\n\nfabric\n Fabric for the subnet. Defaults to the fabric the\n provided VLAN belongs to, or defaults to the default fabric.\n\nvid\n VID of the VLAN this subnet belongs to. Only used when vlan is\n not provided. Picks the VLAN with this VID in the provided\n fabric or the default fabric if one is not given.\n\nspace\n Space this subnet is in. Defaults to the default space.\n\ngateway_ip\n The gateway IP address for this subnet.\n\nrdns_mode\n How reverse DNS is handled for this subnet.\n One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled\n means no reverse zone is created; Enabled means generate the\n reverse zone; RFC2317 extends Enabled to create the necessary\n parent zone with the appropriate CNAME resource records for the\n network, if the network is small enough to require the support\n described in RFC2317.\n\nallow_proxy\n Configure maas-proxy to allow requests from this\n subnet.\n\ndns_servers\n Comma-seperated list of DNS servers for this subnet.\n\nmanaged\n In MAAS 2.0+, all subnets are assumed to be managed by default.\n\n Only managed subnets allow DHCP to be enabled on their related\n dynamic ranges. (Thus, dynamic ranges become \"informational\n only\"; an indication that another DHCP server is currently\n handling them, or that MAAS will handle them when the subnet is\n enabled for management.)\n\n Managed subnets do not allow IP allocation by default. The\n meaning of a \"reserved\" IP range is reversed for an unmanaged\n subnet. (That is, for managed subnets, \"reserved\" means \"MAAS\n cannot allocate any IP address within this reserved block\". For\n unmanaged subnets, \"reserved\" means \"MAAS must allocate IP\n addresses only from reserved IP ranges\".", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all subnets.", "op": null}], "doc": "Manage subnets.", "uri": "http://localhost:5240/MAAS/api/2.0/subnets/", "path": "/MAAS/api/2.0/subnets/"}}, {"name": "BlockDevicesHandler", "anon": null, "auth": {"name": "BlockDevicesHandler", "params": ["system_id"], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a physical block device.\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be\n provided. This should be a path that is fixed and doesn't change\n depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all block devices belonging to a machine.\n\nReturns 404 if the machine is not found.", "op": null}], "doc": "Manage block devices on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/"}}, {"name": "InterfaceHandler", "anon": null, "auth": {"name": "InterfaceHandler", "params": ["system_id", "id"], "actions": [{"restful": false, "name": "link_subnet", "method": "POST", "doc": "Link interface to a subnet.\n\n:param mode: AUTO, DHCP, STATIC or LINK_UP connection to subnet.\n:param subnet: Subnet linked to interface.\n:param ip_address: IP address for the interface in subnet. Only used\n when mode is STATIC. If not provided an IP address from subnet\n will be auto selected.\n:param force: If True, allows LINK_UP to be set on the interface\n even if other links already exist. Also allows the selection of any\n VLAN, even a VLAN MAAS does not believe the interface to currently\n be on. Using this option will cause all other links on the\n interface to be deleted. (Defaults to False.)\n:param default_gateway: True sets the gateway IP address for the subnet\n as the default gateway for the node this interface belongs to.\n Option can only be used with the AUTO and STATIC modes.\n\nMode definitions:\nAUTO - Assign this interface a static IP address from the provided\nsubnet. The subnet must be a managed subnet. The IP address will\nnot be assigned until the node goes to be deployed.\n\nDHCP - Bring this interface up with DHCP on the given subnet. Only\none subnet can be set to DHCP. If the subnet is managed this\ninterface will pull from the dynamic IP range.\n\nSTATIC - Bring this interface up with a STATIC IP address on the\ngiven subnet. Any number of STATIC links can exist on an interface.\n\nLINK_UP - Bring this interface up only on the given subnet. No IP\naddress will be assigned to this interface. The interface cannot\nhave any current AUTO, DHCP or STATIC links.\n\nReturns 404 if the node or interface is not found.", "op": "link_subnet"}, {"restful": false, "name": "remove_tag", "method": "POST", "doc": "Remove a tag from interface on a node.\n\n:param tag: The tag being removed.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.", "op": "remove_tag"}, {"restful": false, "name": "unlink_subnet", "method": "POST", "doc": "Unlink interface to a subnet.\n\n:param id: ID of the link on the interface to remove.\n\nReturns 404 if the node or interface is not found.", "op": "unlink_subnet"}, {"restful": false, "name": "set_default_gateway", "method": "POST", "doc": "Set the node to use this interface as the default gateway.\n\nIf this interface has more than one subnet with a gateway IP in the\nsame IP address family then specifying the ID of the link on\nthis interface is required.\n\n:param link_id: ID of the link on this interface to select the\n default gateway IP address from.\n\nReturns 400 if the interface has not AUTO or STATIC links.\nReturns 404 if the node or interface is not found.", "op": "set_default_gateway"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete interface on node.\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read interface on node.\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update interface on node.\n\nMachines must has status of Ready or Broken to have access to all\noptions. Machines with Deployed status can only have the name and/or\nmac_address updated for an interface. This is intented to allow a bad\ninterface to be replaced while the machine remains deployed.\n\nFields for physical interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n\nFields for bond interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not set\n then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFields for VLAN interface:\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFields for bridge interface:\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are extra parameters that can be set on all interface types:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n\nSupported bonding modes (bond-mode):\n\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nReturns 404 if the node or interface is not found.", "op": null}, {"restful": false, "name": "disconnect", "method": "POST", "doc": "Disconnect an interface.\n\nDeletes any linked subnets and IP addresses, and disconnects the\ninterface from any associated VLAN.\n\nReturns 404 if the node or interface is not found.", "op": "disconnect"}, {"restful": false, "name": "add_tag", "method": "POST", "doc": "Add a tag to interface on a node.\n\n:param tag: The tag being added.\n\nReturns 404 if the node or interface is not found.\nReturns 403 if the user is not allowed to update the interface.", "op": "add_tag"}], "doc": "Manage a node's or device's interface.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/{id}/"}}, {"name": "PodHandler", "anon": null, "auth": {"name": "PodHandler", "params": ["id"], "actions": [{"restful": false, "name": "parameters", "method": "GET", "doc": "Obtain pod parameters.\n\nThis method is reserved for admin users and returns a 403 if the\nuser is not one.\n\nThis returns the pod parameters, if any, configured for a\npod. For some types of pod this will include private\ninformation such as passwords and secret keys.\n\nReturns 404 if the pod is not found.", "op": "parameters"}, {"restful": false, "name": "refresh", "method": "POST", "doc": "Refresh a specific Pod.\n\nPerforms pod discovery and updates all discovered information and\ndiscovered machines.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to refresh the pod.", "op": "refresh"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete a specific Pod.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to delete the pod.\nReturns 204 if the pod is successfully deleted.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": null, "op": null}, {"restful": false, "name": "compose", "method": "POST", "doc": "Compose a machine from Pod.\n\nAll fields below are optional:\n\n:param cores: Minimum number of CPU cores.\n:param memory: Minimum amount of memory (MiB).\n:param cpu_speed: Minimum amount of CPU speed (MHz).\n:param architecture: Architecture for the machine. Must be an\n architecture that the pod supports.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to compose machine.", "op": "compose"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update a specific Pod.\n\n:param name: Name for the pod (optional).\n\nNote: 'type' cannot be updated on a Pod. The Pod must be deleted and\nre-added to change the type.\n\nReturns 404 if the pod is not found.\nReturns 403 if the user does not have permission to update the pod.", "op": null}], "doc": "Manage an individual pod.\n\nThe pod is identified by its id.", "uri": "http://localhost:5240/MAAS/api/2.0/pods/{id}/", "path": "/MAAS/api/2.0/pods/{id}/"}}, {"name": "ZoneHandler", "anon": null, "auth": {"name": "ZoneHandler", "params": ["name"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "GET request. Return zone.\n\nReturns 404 if the zone is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "PUT request. Update zone.\n\nReturns 404 if the zone is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "DELETE request. Delete zone.\n\nReturns 404 if the zone is not found.\nReturns 204 if the zone is successfully deleted.", "op": null}], "doc": "Manage a physical zone.\n\nAny node is in a physical zone, or \"zone\" for short. The meaning of a\nphysical zone is up to you: it could identify e.g. a server rack, a\nnetwork, or a data centre. Users can then allocate nodes from specific\nphysical zones, to suit their redundancy or performance requirements.\n\nThis functionality is only available to administrators. Other users can\nview physical zones, but not modify them.", "uri": "http://localhost:5240/MAAS/api/2.0/zones/{name}/", "path": "/MAAS/api/2.0/zones/{name}/"}}, {"name": "IPAddressesHandler", "anon": null, "auth": {"name": "IPAddressesHandler", "params": [], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "List IP addresses known to MAAS.\n\nBy default, gets a listing of all IP addresses allocated to the\nrequesting user.\n\n:param ip: If specified, will only display information for the\n specified IP address.\n:type ip: unicode (must be an IPv4 or IPv6 address)\n\nIf the requesting user is a MAAS administrator, the following options\nmay also be supplied:\n\n:param all: If True, all reserved IP addresses will be shown. (By\n default, only addresses of type 'User reserved' that are assigned\n to the requesting user are shown.)\n:type all: bool\n\n:param owner: If specified, filters the list to show only IP addresses\n owned by the specified username.\n:type user: unicode", "op": null}, {"restful": false, "name": "reserve", "method": "POST", "doc": "Reserve an IP address for use outside of MAAS.\n\nReturns an IP adddress, which MAAS will not allow any of its known\nnodes to use; it is free for use by the requesting user until released\nby the user.\n\nThe user may supply either a subnet or a specific IP address within a\nsubnet.\n\n:param subnet: CIDR representation of the subnet on which the IP\n reservation is required. e.g. 10.1.2.0/24\n:param ip: The IP address, which must be within\n a known subnet.\n:param ip_address: (Deprecated.) Alias for 'ip' parameter. Provided\n for backward compatibility.\n:param hostname: The hostname to use for the specified IP address. If\n no domain component is given, the default domain will be used.\n:param mac: The MAC address that should be linked to this reservation.\n\nReturns 400 if there is no subnet in MAAS matching the provided one,\nor a ip_address is supplied, but a corresponding subnet\ncould not be found.\nReturns 503 if there are no more IP addresses available.", "op": "reserve"}, {"restful": false, "name": "release", "method": "POST", "doc": "Release an IP address that was previously reserved by the user.\n\n:param ip: The IP address to release.\n:type ip: unicode\n\n:param force: If True, allows a MAAS administrator to force an IP\n address to be released, even if it is not a user-reserved IP\n address or does not belong to the requesting user. Use with\n caution.\n:type force: bool\n\nReturns 404 if the provided IP address is not found.", "op": "release"}], "doc": "Manage IP addresses allocated by MAAS.", "uri": "http://localhost:5240/MAAS/api/2.0/ipaddresses/", "path": "/MAAS/api/2.0/ipaddresses/"}}, {"name": "IPRangeHandler", "anon": null, "auth": {"name": "IPRangeHandler", "params": ["id"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read IP range.\n\nReturns 404 if the IP range is not found.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update IP range.\n\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param comment: A description of this range. (optional)\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP Range is not found.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete IP range.\n\nReturns 403 if not owner of IP range.\nReturns 404 if the IP range is not found.", "op": null}], "doc": "Manage IP range.", "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/{id}/", "path": "/MAAS/api/2.0/ipranges/{id}/"}}, {"name": "LicenseKeyHandler", "anon": null, "auth": {"name": "LicenseKeyHandler", "params": ["osystem", "distro_series"], "actions": [{"restful": true, "name": "read", "method": "GET", "doc": "Read license key.", "op": null}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update license key.\n\n:param osystem: Operating system that the key belongs to.\n:param distro_series: OS release that the key belongs to.\n:param license_key: License key for osystem/distro_series combo.", "op": null}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete license key.", "op": null}], "doc": "Manage a license key.", "uri": "http://localhost:5240/MAAS/api/2.0/license-key/{osystem}/{distro_series}", "path": "/MAAS/api/2.0/license-key/{osystem}/{distro_series}"}}, {"name": "BlockDeviceHandler", "anon": null, "auth": {"name": "BlockDeviceHandler", "params": ["system_id", "id"], "actions": [{"restful": false, "name": "remove_tag", "method": "POST", "doc": "Remove a tag from block device on a machine.\n\n:param tag: The tag being removed.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": "remove_tag"}, {"restful": false, "name": "mount", "method": "POST", "doc": "Mount the filesystem on block device.\n\n:param mount_point: Path on the filesystem to mount.\n:param mount_options: Options to pass to mount(8).\n\nReturns 403 when the user doesn't have the ability to mount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "mount"}, {"restful": true, "name": "delete", "method": "DELETE", "doc": "Delete block device on a machine.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to delete the block device.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": false, "name": "set_boot_disk", "method": "POST", "doc": "Set this block device as the boot disk for the machine.\n\nReturns 400 if the block device is a virtual block device.\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready or Allocated.", "op": "set_boot_disk"}, {"restful": true, "name": "update", "method": "PUT", "doc": "Update block device on a machine.\n\nMachines must have a status of Ready to have access to all options.\nMachines with Deployed status can only have the name, model, serial,\nand/or id_path updated for a block device. This is intented to allow a\nbad block device to be replaced while the machine remains deployed.\n\nFields for physical block device:\n\n:param name: Name of the block device.\n:param model: Model of the block device.\n:param serial: Serial number of the block device.\n:param id_path: (optional) Only used if model and serial cannot be provided. This should be a path that is fixed and doesn't change depending on the boot order or kernel version.\n:param size: Size of the block device.\n:param block_size: Block size of the block device.\n\nFields for virtual block device:\n\n:param name: Name of the block device.\n:param uuid: UUID of the block device.\n:param size: Size of the block device. (Only allowed for logical volumes.)\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "Read block device on node.\n\nReturns 404 if the machine or block device is not found.", "op": null}, {"restful": false, "name": "unformat", "method": "POST", "doc": "Unformat block device with filesystem.\n\nReturns 400 if the block device is not formatted, currently mounted, or part of a filesystem group.\nReturns 403 when the user doesn't have the ability to unformat the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "unformat"}, {"restful": false, "name": "format", "method": "POST", "doc": "Format block device with filesystem.\n\n:param fstype: Type of filesystem.\n:param uuid: UUID of the filesystem.\n\nReturns 403 when the user doesn't have the ability to format the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "format"}, {"restful": false, "name": "add_tag", "method": "POST", "doc": "Add a tag to block device on a machine.\n\n:param tag: The tag being added.\n\nReturns 404 if the machine or block device is not found.\nReturns 403 if the user is not allowed to update the block device.\nReturns 409 if the machine is not Ready.", "op": "add_tag"}, {"restful": false, "name": "unmount", "method": "POST", "doc": "Unmount the filesystem on block device.\n\nReturns 400 if the block device is not formatted or not currently mounted.\nReturns 403 when the user doesn't have the ability to unmount the block device.\nReturns 404 if the machine or block device is not found.\nReturns 409 if the machine is not Ready or Allocated.", "op": "unmount"}], "doc": "Manage a block device on a machine.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/", "path": "/MAAS/api/2.0/nodes/{system_id}/blockdevices/{id}/"}}, {"name": "InterfacesHandler", "anon": null, "auth": {"name": "InterfacesHandler", "params": ["system_id"], "actions": [{"restful": false, "name": "create_bridge", "method": "POST", "doc": "Create a bridge interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to.\n:param parent: Parent interface for this bridge interface.\n\nFollowing are parameters specific to bridges:\n\n:param bridge_stp: Turn spanning tree protocol on or off.\n (Default: False).\n:param bridge_fd: Set bridge forward delay to time seconds.\n (Default: 15).\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_bridge"}, {"restful": false, "name": "create_physical", "method": "POST", "doc": "Create a physical interface on a machine and device.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: Untagged VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_physical"}, {"restful": false, "name": "create_vlan", "method": "POST", "doc": "Create a VLAN interface on a machine.\n\n:param tags: Tags for the interface.\n:param vlan: Tagged VLAN the interface is connected to.\n:param parent: Parent interface for this VLAN interface.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_vlan"}, {"restful": true, "name": "read", "method": "GET", "doc": "List all interfaces belonging to a machine, device, or\nrack controller.\n\nReturns 404 if the node is not found.", "op": null}, {"restful": false, "name": "create_bond", "method": "POST", "doc": "Create a bond interface on a machine.\n\n:param name: Name of the interface.\n:param mac_address: MAC address of the interface.\n:param tags: Tags for the interface.\n:param vlan: VLAN the interface is connected to. If not\n provided then the interface is considered disconnected.\n:param parents: Parent interfaces that make this bond.\n\nFollowing are parameters specific to bonds:\n\n:param bond_mode: The operating mode of the bond.\n (Default: active-backup).\n:param bond_miimon: The link monitoring freqeuncy in milliseconds.\n (Default: 100).\n:param bond_downdelay: Specifies the time, in milliseconds, to wait\n before disabling a slave after a link failure has been detected.\n:param bond_updelay: Specifies the time, in milliseconds, to wait\n before enabling a slave after a link recovery has been detected.\n:param bond_lacp_rate: Option specifying the rate in which we'll ask\n our link partner to transmit LACPDU packets in 802.3ad mode.\n Available options are fast or slow. (Default: slow).\n:param bond_xmit_hash_policy: The transmit hash policy to use for\n slave selection in balance-xor, 802.3ad, and tlb modes.\n (Default: layer2)\n\nSupported bonding modes (bond-mode):\nbalance-rr - Transmit packets in sequential order from the first\navailable slave through the last. This mode provides load balancing\nand fault tolerance.\n\nactive-backup - Only one slave in the bond is active. A different\nslave becomes active if, and only if, the active slave fails. The\nbond's MAC address is externally visible on only one port (network\nadapter) to avoid confusing the switch.\n\nbalance-xor - Transmit based on the selected transmit hash policy.\nThe default policy is a simple [(source MAC address XOR'd with\ndestination MAC address XOR packet type ID) modulo slave count].\n\nbroadcast - Transmits everything on all slave interfaces. This mode\nprovides fault tolerance.\n\n802.3ad - IEEE 802.3ad Dynamic link aggregation. Creates aggregation\ngroups that share the same speed and duplex settings. Utilizes all\nslaves in the active aggregator according to the 802.3ad specification.\n\nbalance-tlb - Adaptive transmit load balancing: channel bonding that\ndoes not require any special switch support.\n\nbalance-alb - Adaptive load balancing: includes balance-tlb plus\nreceive load balancing (rlb) for IPV4 traffic, and does not require any\nspecial switch support. The receive load balancing is achieved by\nARP negotiation.\n\nFollowing are extra parameters that can be set on the interface:\n\n:param mtu: Maximum transmission unit.\n:param accept_ra: Accept router advertisements. (IPv6 only)\n:param autoconf: Perform stateless autoconfiguration. (IPv6 only)\n\nReturns 404 if the node is not found.", "op": "create_bond"}], "doc": "Manage interfaces on a node.", "uri": "http://localhost:5240/MAAS/api/2.0/nodes/{system_id}/interfaces/", "path": "/MAAS/api/2.0/nodes/{system_id}/interfaces/"}}, {"name": "ZonesHandler", "anon": null, "auth": {"name": "ZonesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create a new physical zone.\n\n:param name: Identifier-style name for the new zone.\n:type name: unicode\n:param description: Free-form description of the new zone.\n:type description: unicode", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List zones.\n\nGet a listing of all the physical zones.", "op": null}], "doc": "Manage physical zones.", "uri": "http://localhost:5240/MAAS/api/2.0/zones/", "path": "/MAAS/api/2.0/zones/"}}, {"name": "IPRangesHandler", "anon": null, "auth": {"name": "IPRangesHandler", "params": [], "actions": [{"restful": true, "name": "create", "method": "POST", "doc": "Create an IP range.\n\n:param type: Type of this range. (`dynamic` or `reserved`)\n:param start_ip: Start IP address of this range (inclusive).\n:param end_ip: End IP address of this range (inclusive).\n:param subnet: Subnet this range is associated with. (optional)\n:param comment: A description of this range. (optional)\n\nReturns 403 if standard users tries to create a dynamic IP range.", "op": null}, {"restful": true, "name": "read", "method": "GET", "doc": "List all IP ranges.", "op": null}], "doc": "Manage IP ranges.", "uri": "http://localhost:5240/MAAS/api/2.0/ipranges/", "path": "/MAAS/api/2.0/ipranges/"}}]}
\ No newline at end of file
diff --git a/maas/client/bones/testing/desc.py b/maas/client/bones/testing/desc.py
new file mode 100644
index 00000000..46f68a46
--- /dev/null
+++ b/maas/client/bones/testing/desc.py
@@ -0,0 +1,179 @@
+"""Abstractions around API description documents."""
+
+__all__ = ["Description", "Action"]
+
+from keyword import iskeyword
+from operator import itemgetter
+
+from maas.client.bones.helpers import derive_resource_name
+from maas.client.utils import parse_docstring
+
+
+class Description:
+ """Object-oriented interface to a MAAS API description document."""
+
+ def __init__(self, description):
+ super(Description, self).__init__()
+ self._description = description
+ self._populate()
+
+ def _populate(self):
+ self.anon = Resources(True, self._resources("anon"))
+ self.auth = Resources(False, self._resources("auth"))
+
+ def _resources(self, classification):
+ resources = self._description["resources"]
+ resources = map(itemgetter(classification), resources)
+ return (rs for rs in resources if rs is not None)
+
+ @property
+ def doc(self):
+ doc = self._description["doc"]
+ return parse_docstring(doc)
+
+ @property
+ def hash(self):
+ return self._description["hash"]
+
+ @property
+ def raw(self):
+ return self._description
+
+ def __iter__(self):
+ """Iterate all resources, anonymous first."""
+ yield from self.anon
+ yield from self.auth
+
+ def __repr__(self):
+ title, body = self.doc
+ return "<%s %r %s>" % (self.__class__.__name__, title.rstrip("."), self.hash)
+
+
+class Resources:
+ """Pincushion of API resources."""
+
+ def __init__(self, is_anonymous, resources):
+ super(Resources, self).__init__()
+ for resource in resources:
+ name = derive_resource_name(resource["name"])
+ resource = Resource(name, is_anonymous, resource)
+ attrname = "%s_" % name if iskeyword(name) else name
+ setattr(self, attrname, resource)
+
+ def __iter__(self):
+ """Iterate all resources."""
+ for value in vars(self).values():
+ if isinstance(value, Resource):
+ yield value
+
+
+class Resource:
+ """An API resource, like `Machines`."""
+
+ def __init__(self, name, is_anonymous, data):
+ super(Resource, self).__init__()
+ self._is_anonymous = is_anonymous
+ self._name = name
+ self._data = data
+ self._populate()
+
+ def _populate(self):
+ for action in self._data["actions"]:
+ name = action["name"]
+ name = "%s_" % name if iskeyword(name) else name
+ setattr(self, name, Action(self, action))
+ self._properties = {
+ "doc": parse_docstring(self._data["doc"]),
+ "is_anonymous": self._is_anonymous,
+ "name": self._name,
+ "name/raw": self._data["name"],
+ "params": tuple(self._data["params"]),
+ "path": self._data["path"],
+ "uri": self._data["uri"],
+ }
+
+ def __getitem__(self, name):
+ return self._properties[name]
+
+ def __iter__(self):
+ """Iterate all actions."""
+ for value in vars(self).values():
+ if isinstance(value, Action):
+ yield value
+
+ def __repr__(self):
+ title, body = self["doc"]
+ return "<%s:%s %r>" % (self.__class__.__name__, self._name, title.rstrip("."))
+
+
+class Action:
+ """An API action on a resource, like `Machines.allocate`."""
+
+ def __init__(self, resource, data):
+ super(Action, self).__init__()
+ self._resource = resource
+ self._data = data
+
+ # Resource-specific properties.
+
+ @property
+ def resource(self):
+ return self._resource
+
+ @property
+ def is_anonymous(self):
+ return self._resource["is_anonymous"]
+
+ @property
+ def params(self):
+ return self._resource["params"]
+
+ @property
+ def path(self):
+ return self._resource["path"]
+
+ @property
+ def uri(self):
+ return self._resource["uri"]
+
+ # Action-specific properties.
+
+ @property
+ def doc(self):
+ doc = self._data["doc"]
+ return parse_docstring(doc)
+
+ @property
+ def method(self):
+ return self._data["method"]
+
+ @property
+ def name(self):
+ return self._data["name"]
+
+ @property
+ def op(self):
+ return self._data["op"]
+
+ @property
+ def is_restful(self):
+ return self._data["restful"]
+
+ @property
+ def action_name(self):
+ anon_auth = "anon" if self.is_anonymous else "auth"
+ return "%s:%s.%s" % (anon_auth, self.resource["name"], self.name)
+
+ # Other.
+
+ def __repr__(self):
+ title, body = self.doc
+ return "<%s:%s.%s %r %s %s%s>" % (
+ self.__class__.__name__,
+ self._resource._name,
+ self.name,
+ title.rstrip("."),
+ self.method,
+ self.path,
+ ("" if self.op is None else "?op=" + self.op),
+ )
diff --git a/maas/client/bones/testing/server.py b/maas/client/bones/testing/server.py
new file mode 100644
index 00000000..15648e89
--- /dev/null
+++ b/maas/client/bones/testing/server.py
@@ -0,0 +1,335 @@
+"""Testing server."""
+
+__all__ = ["ApplicationBuilder"]
+
+import asyncio
+from collections import defaultdict
+from functools import partial
+import json
+from operator import attrgetter
+import re
+from urllib.parse import urlparse
+
+from aiohttp.multipart import CONTENT_DISPOSITION, parse_content_disposition
+import aiohttp.web
+from multidict import MultiDict
+
+from . import desc
+
+
+class ApplicationBuilder:
+ 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._wire_up_description()
+ self._actions = {}
+ self._views = {}
+
+ def route(self, method, path, handler=None):
+ """Add a handler for a specific path.
+
+ The path must start with a forward slash or be empty. The root URL of
+ the server is automatically discovered from the API description and is
+ added as a prefix to `path`, i.e. it's correct to specify "/accounts/"
+ but incorrect to specify "/MAAS/accounts/".
+
+ The handler is wrapped by `_wrap_handler` but should otherwise a normal
+ `aiohttp` request handler.
+
+ This method can be used as a decorator by omitting `handler`:
+
+ @builder.route("GET", "/foo/bar")
+ async def foobar(request):
+ ...
+
+ The use of `handle` should be preferred to `route` where the endpoint
+ is part of MAAS's Web API. This method allows mocking of miscellaneous
+ other paths when absolutely necessary.
+ """
+ if handler is None:
+ return partial(self.route, method, path)
+
+ if not path.startswith("/"):
+ raise ValueError("Path should start with / or be empty.")
+
+ handler = self._wrap_handler(handler)
+ path = "%s/%s" % (self._rootpath, path.lstrip("/"))
+ self._application.router.add_route(method, path, handler)
+
+ def handle(self, action_name, handler=None):
+ """Add a handler for a specific API action.
+
+ The action string describes an action from the server's API
+ description document. Some examples:
+
+ auth:Machines.allocate
+ anon:Version.read
+
+ The handler is wrapped by `_wrap_handler` but should otherwise a normal
+ `aiohttp` request handler.
+
+ This method can be used as a decorator by omitting `handler`:
+
+ @builder.handle("anon:Version.read")
+ async def version(request):
+ ...
+
+ """
+ if handler is None:
+ return partial(self.handle, action_name)
+
+ action = self._resolve_action(action_name)
+ view_name = self._view_name(action)
+ assert view_name not in self._actions
+ self._actions[view_name] = action
+ handler = self._wrap_handler(handler)
+ if view_name in self._views:
+ view = self._views[view_name]
+ view.set(action, handler)
+ else:
+ view = self._views[view_name] = ApplicationView()
+ self._application.router.add_route("*", action.path, view)
+ view.set(action, handler)
+
+ def serve(self):
+ """Return an async context manager to serve the built application."""
+ return ApplicationRunner(self._application, self._basepath)
+
+ @staticmethod
+ def _wrap_handler(handler):
+ """Wrap `handler` in some conveniences.
+
+ These are:
+
+ * Setting `request.params` to a `MultiDict` instance of POSTed form
+ parameters, or `None` if the body content was not a multipart form.
+
+ * Passing `request.match_info` as keyword arguments into the handler.
+ For example, if a route like "/foo/{bar}" is matched by a path
+ "/foo/thing", the handler will be called with bar="thing".
+
+ * Objects returned from `handler` that are not proper responses are
+ rendered as JSON.
+
+ """
+
+ async def wrapper(request):
+ # For convenience, read in all multipart parameters.
+ assert not hasattr(request, "params")
+ if request.content_type == "multipart/form-data":
+ request.params = await _get_multipart_params(request)
+ else:
+ request.params = None
+ response = await handler(request, **request.match_info)
+ # For convenience, assume non-Responses are meant as JSON.
+ if not isinstance(response, aiohttp.web.Response):
+ response = aiohttp.web.json_response(response)
+ return response
+
+ return wrapper
+
+ @staticmethod
+ def _view_name(action):
+ """Return the view name for an API action, e.g. "Version.read"."""
+ return "%s.%s" % (action.resource["name"], action.name)
+
+ def _discover_version_and_paths(self):
+ """Return the root path, the API path, and the API version string.
+
+ As an example, given an API description document containing references
+ to "/MAAS/api/2.0/foo/bar", this function will return:
+
+ ("/MAAS", "/MAAS/api/2.0", "2.0")
+
+ """
+ for resource in self._description:
+ path = urlparse(resource["uri"]).path
+ match = re.match("((.*)/api/([0-9.]+))/", path)
+ if match is not None:
+ base, root, version = match.groups()
+ return root, base, version
+ else:
+ raise ValueError("Could not discover version or paths.")
+
+ def _wire_up_description(self):
+ """Arrange for the API description document to be served.
+
+ This publishes only endpoints for which handlers have been registered
+ using `handle`.
+ """
+ path = "%s/describe/" % self._basepath
+
+ def describe(request):
+ description = self._render_description(request.url.with_path(""))
+ description_json = json.dumps(description, indent=" ", sort_keys=True)
+ return aiohttp.web.Response(
+ text=description_json, content_type="application/json"
+ )
+
+ self._application.router.add_get(path, describe)
+
+ def _render_description(self, base):
+ """Render an API description document for this application.
+
+ This renders only endpoints for which handlers have been registered
+ using `handle`.
+ """
+ by_resource = defaultdict(list)
+ for action in self._actions.values():
+ by_resource[action.resource].append(action)
+
+ def make_resource_skeleton():
+ # The "names" set gets popped later and replaced.
+ return {"anon": None, "auth": None, "names": set()}
+
+ by_resource_name = defaultdict(make_resource_skeleton)
+ for resource, actions in by_resource.items():
+ res_name = resource["name"]
+ res_name_raw = resource["name/raw"]
+ res_desc = by_resource_name[res_name]
+ res_desc["names"].add(res_name_raw)
+ anon_auth = "anon" if resource["is_anonymous"] else "auth"
+ assert res_desc[anon_auth] is None
+ res_desc[anon_auth] = {
+ "actions": [
+ {
+ "doc": action.doc.title, # Just the title.
+ "method": action.method,
+ "name": action.name,
+ "op": action.op,
+ "restful": action.is_restful,
+ }
+ for action in actions
+ ],
+ "doc": resource["doc"].title,
+ "name": res_name_raw,
+ "params": resource["params"],
+ "path": resource["path"],
+ "uri": str(base) + resource["path"],
+ }
+
+ for res_desc in by_resource_name.values():
+ res_names = res_desc.pop("names")
+ res_desc["name"] = min(res_names, key=len)
+
+ return {
+ "doc": self._description.doc.title,
+ "hash": "// not calculated //",
+ "resources": list(by_resource_name.values()),
+ }
+
+ def _resolve_action(self, action_name):
+ """Find information on the given action.
+
+ For example, given "anon:Version.read" it would return a `desc.Action`
+ instance describing the anonymous "read" action on "VersionHandler".
+ """
+ match = re.match(r"^(anon|auth):(\w+[.]\w+)$", action_name)
+ if match is None:
+ raise ValueError(
+ "Action should be (anon|auth):Resource.action, got: %s" % (action_name,)
+ )
+ else:
+ anon_auth, resource_name = match.groups()
+ resources = getattr(self._description, anon_auth)
+ try:
+ action = attrgetter(resource_name)(resources)
+ except AttributeError:
+ raise ValueError("%s not found." % resource_name)
+ else:
+ assert action.action_name == action_name
+ return action
+
+
+class ApplicationView:
+ """A meta-handler that mimics the behaviour of MAAS's Web API handlers."""
+
+ def __init__(self):
+ super(ApplicationView, self).__init__()
+ self.rest, self.ops = {}, {}
+
+ @property
+ def allowed_methods(self):
+ allowed_methods = frozenset(self.rest)
+ if len(self.ops) == 0:
+ return allowed_methods
+ else:
+ return allowed_methods | {aiohttp.hdrs.METH_POST}
+
+ def set(self, action, handler):
+ if action.is_restful:
+ self.rest[action.method] = handler
+ else:
+ self.ops[action.op] = handler
+
+ async def __call__(self, request):
+ if request.method == "POST":
+ op = request.rel_url.query.get("op")
+ if op is None:
+ handler = self.rest.get(request.method)
+ else:
+ handler = self.ops.get(op)
+ else:
+ handler = self.rest.get(request.method)
+
+ if handler is None:
+ raise aiohttp.web.HTTPMethodNotAllowed(request.method, self.allowed_methods)
+ else:
+ return await handler(request)
+
+
+class ApplicationRunner:
+ """An asynchronous context manager that starts and stops an application."""
+
+ def __init__(self, application, basepath):
+ super(ApplicationRunner, self).__init__()
+ self._application = application
+ self._basepath = basepath
+
+ async def __aenter__(self):
+ self._loop = asyncio.get_event_loop()
+ self._handler = self._application.make_handler(loop=self._loop)
+ await self._application.startup()
+ self._server = await self._loop.create_server(
+ self._handler, host="0.0.0.0", port=0
+ )
+ return "http://%s:%d/%s/" % (
+ *self._server.sockets[0].getsockname(),
+ self._basepath.strip("/"),
+ )
+
+ async def __aexit__(self, *exc_info):
+ self._server.close()
+ await self._server.wait_closed()
+ await self._application.shutdown()
+ await self._handler.shutdown(10.0)
+ await self._application.cleanup()
+
+
+async def _get_multipart_params(request):
+ """Extract a mapping of parts sent in a multipart request.
+
+ :rtype: MultiDict
+ """
+
+ def get_part_name(part):
+ _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION))
+ return params.get("name")
+
+ def get_part_data(part):
+ if part.filename is None:
+ return part.text()
+ else:
+ return part.read(decode=True)
+
+ params = MultiDict()
+ async for part in await request.multipart():
+ params.add(get_part_name(part), await get_part_data(part))
+
+ return params
diff --git a/maas/client/bones/tests/test.py b/maas/client/bones/tests/test.py
new file mode 100644
index 00000000..1d7fa665
--- /dev/null
+++ b/maas/client/bones/tests/test.py
@@ -0,0 +1,151 @@
+"""Tests for `maas.client.bones`."""
+
+import json
+import random
+from unittest.mock import ANY, Mock
+from urllib.parse import parse_qsl, urlparse
+from uuid import uuid1
+
+from testtools.matchers import Equals, Is, MatchesStructure
+
+from .. import testing
+from ... import bones
+from ...testing import TestCase
+from ...utils.tests.test_auth import make_Credentials
+
+
+class TestSessionAPI(TestCase):
+ def test__fromURL_raises_SessionError_when_request_fails(self):
+ fixture = self.useFixture(testing.DescriptionServer(b"bogus"))
+ error = self.assertRaises(
+ bones.SessionError, bones.SessionAPI.fromURL, fixture.url + "bogus/"
+ )
+ self.assertEqual(fixture.url + "bogus/ -> 404 Not Found", str(error))
+
+ def test__fromURL_raises_SessionError_when_content_not_json(self):
+ fixture = self.useFixture(testing.DescriptionServer())
+ fixture.handler.content_type = "text/json"
+ error = self.assertRaises(
+ bones.SessionError, bones.SessionAPI.fromURL, fixture.url
+ )
+ self.assertEqual("Expected application/json, got: text/json", str(error))
+
+ async def test__fromURL_sets_credentials_on_session(self):
+ fixture = self.useFixture(testing.DescriptionServer())
+ credentials = make_Credentials()
+ session = await bones.SessionAPI.fromURL(fixture.url, credentials=credentials)
+ self.assertIs(credentials, session.credentials)
+
+ async def test__fromURL_sets_insecure_on_session(self):
+ insecure = random.choice((True, False))
+ fixture = self.useFixture(testing.DescriptionServer())
+ session = await bones.SessionAPI.fromURL(fixture.url, insecure=insecure)
+ self.assertThat(session.insecure, Is(insecure))
+
+ async def test__fromURL_sets_scheme_on_session(self):
+ insecure = random.choice((True, False))
+ fixture = self.useFixture(testing.DescriptionServer())
+ session = await bones.SessionAPI.fromURL(fixture.url, insecure=insecure)
+ self.assertThat(session.scheme, Equals("http"))
+
+
+class TestSessionAPI_APIVersions(TestCase):
+ """Tests for `SessionAPI` with multiple API versions."""
+
+ scenarios = tuple(
+ (name, dict(version=version, path=path))
+ for name, version, path in testing.list_api_descriptions()
+ )
+
+ async def test__fromURL_downloads_description(self):
+ description = self.path.read_bytes()
+ fixture = self.useFixture(testing.DescriptionServer(description))
+ session = await bones.SessionAPI.fromURL(fixture.url)
+ self.assertEqual(json.loads(description.decode("utf-8")), session.description)
+
+
+class TestActionAPI_APIVersions(TestCase):
+ """Tests for `ActionAPI` with multiple API versions."""
+
+ scenarios = tuple(
+ (name, dict(version=version, description=description))
+ 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.url, self.description)
+ action = session.Version.read
+ self.assertThat(
+ action,
+ MatchesStructure.byEquality(
+ name="read",
+ fullname="Version.read",
+ method="GET",
+ handler=session.Version,
+ is_restful=True,
+ op=None,
+ ),
+ )
+
+ def test__Machines_deployment_status(self):
+ if self.version > (2, 0):
+ self.skipTest("Machines.deployment_status only in <= 2.0")
+
+ session = bones.SessionAPI(self.url, self.description, ("a", "b", "c"))
+ action = session.Machines.deployment_status
+ self.assertThat(
+ action,
+ MatchesStructure.byEquality(
+ name="deployment_status",
+ fullname="Machines.deployment_status",
+ method="GET",
+ handler=session.Machines,
+ is_restful=False,
+ op="deployment_status",
+ ),
+ )
+
+ def test__Machines_power_parameters(self):
+ session = bones.SessionAPI(self.url, self.description, ("a", "b", "c"))
+ action = session.Machines.power_parameters
+ self.assertThat(
+ action,
+ MatchesStructure.byEquality(
+ name="power_parameters",
+ fullname="Machines.power_parameters",
+ method="GET",
+ handler=session.Machines,
+ is_restful=False,
+ op="power_parameters",
+ ),
+ )
+
+
+class TestCallAPI_APIVersions(TestCase):
+ """Tests for `CallAPI` with multiple API versions."""
+
+ scenarios = tuple(
+ (name, dict(version=version, description=description))
+ 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.url, self.description, ("a", "b", "c"))
+ call = session.Machines.power_parameters.bind()
+ call.dispatch = Mock()
+
+ call.call(nodes=system_ids)
+
+ call.dispatch.assert_called_once_with(ANY, ANY, ANY)
+ uri, body, headers = call.dispatch.call_args[0]
+ uri = urlparse(uri)
+ self.assertThat(uri.path, Equals("/MAAS/api/2.0/machines/"))
+ query_expected = [("op", "power_parameters")]
+ query_expected.extend(("nodes", system_id) for system_id in system_ids)
+ query_observed = parse_qsl(uri.query)
+ self.assertThat(query_observed, Equals(query_expected))
diff --git a/maas/client/bones/tests/test_bones.py b/maas/client/bones/tests/test_bones.py
deleted file mode 100644
index 28708d9f..00000000
--- a/maas/client/bones/tests/test_bones.py
+++ /dev/null
@@ -1,221 +0,0 @@
-"""Tests for `maas.client.bones`."""
-
-__all__ = []
-
-from fnmatch import fnmatchcase
-import http
-import http.server
-import json
-from os.path import splitext
-from pathlib import Path
-import re
-import threading
-from unittest.mock import (
- ANY,
- Mock,
- sentinel,
-)
-from urllib.parse import (
- parse_qsl,
- urlparse,
-)
-from uuid import uuid1
-
-import fixtures
-from pkg_resources import (
- resource_filename,
- resource_listdir,
-)
-from testtools.matchers import (
- Equals,
- MatchesStructure,
-)
-
-from ... import bones
-from ...testing import TestCase
-from ...utils.tests.test_auth import make_credentials
-
-
-def list_api_descriptions():
- for filename in resource_listdir(__name__, "."):
- if fnmatchcase(filename, "api*.json"):
- path = resource_filename(__name__, filename)
- name, _ = splitext(filename)
- yield name, Path(path)
-
-
-class DescriptionHandler(http.server.BaseHTTPRequestHandler):
- """An HTTP request handler that serves only API descriptions.
-
- The `desc` attribute ought to be specified, for example by subclassing, or
- by using the `make` class-method.
-
- The `content_type` attribute can be overridden to simulate a different
- Content-Type header for the description.
- """
-
- # Override these in subclasses.
- description = b'{"resources": []}'
- content_type = "application/json"
-
- @classmethod
- def make(cls, description=description):
- return type(
- "DescriptionHandler", (cls, ),
- {"description": description},
- )
-
- def setup(self):
- super(DescriptionHandler, self).setup()
- self.logs = []
-
- def log_message(self, *args):
- """By default logs go to stdout/stderr. Instead, capture them."""
- self.logs.append(args)
-
- def do_GET(self):
- version_match = re.match(r"/MAAS/api/([0-9.]+)/describe/$", self.path)
- if version_match is None:
- self.send_error(http.HTTPStatus.NOT_FOUND)
- else:
- self.send_response(http.HTTPStatus.OK)
- self.send_header("Content-Type", self.content_type)
- self.send_header("Content-Length", str(len(self.description)))
- self.end_headers()
- self.wfile.write(self.description)
-
-
-class DescriptionServer(fixtures.Fixture):
- """Fixture to start up an HTTP server for API descriptions only.
-
- :ivar handler: A `DescriptionHandler` subclass.
- :ivar server: An `http.server.HTTPServer` instance.
- :ivar url: A URL that points to the API that `server` is mocking.
- """
-
- def __init__(self, description=DescriptionHandler.description):
- super(DescriptionServer, self).__init__()
- self.description = description
-
- def _setUp(self):
- self.handler = DescriptionHandler.make(self.description)
- self.server = http.server.HTTPServer(("", 0), self.handler)
- self.url = "http://%s:%d/MAAS/api/2.0/" % self.server.server_address
- threading.Thread(target=self.server.serve_forever).start()
- self.addCleanup(self.server.server_close)
- self.addCleanup(self.server.shutdown)
-
-
-class TestSessionAPI(TestCase):
-
- def test__fromURL_raises_SessionError_when_request_fails(self):
- fixture = self.useFixture(DescriptionServer(b"bogus"))
- error = self.assertRaises(
- bones.SessionError, self.loop.run_until_complete,
- bones.SessionAPI.fromURL(fixture.url + "bogus/"))
- self.assertEqual(
- fixture.url + "bogus/ -> 404 Not Found",
- str(error))
-
- def test__fromURL_raises_SessionError_when_content_not_json(self):
- fixture = self.useFixture(DescriptionServer())
- fixture.handler.content_type = "text/json"
- error = self.assertRaises(
- bones.SessionError, self.loop.run_until_complete,
- bones.SessionAPI.fromURL(fixture.url))
- self.assertEqual(
- "Expected application/json, got: text/json",
- str(error))
-
- def test__fromURL_sets_credentials_on_session(self):
- fixture = self.useFixture(DescriptionServer())
- credentials = make_credentials()
- session = self.loop.run_until_complete(
- bones.SessionAPI.fromURL(fixture.url, credentials=credentials))
- self.assertIs(credentials, session.credentials)
-
- def test__fromURL_sets_insecure_on_session(self):
- fixture = self.useFixture(DescriptionServer())
- session = self.loop.run_until_complete(
- bones.SessionAPI.fromURL(fixture.url, insecure=sentinel.insecure))
- self.assertIs(sentinel.insecure, session.insecure)
-
-
-class TestSessionAPI_APIVersions(TestCase):
- """Tests for `SessionAPI` with multiple API versions."""
-
- scenarios = tuple(
- (name, dict(path=path))
- for name, path in list_api_descriptions()
- )
-
- def test__fromURL_downloads_description(self):
- description = self.path.read_bytes()
- fixture = self.useFixture(DescriptionServer(description))
- session = self.loop.run_until_complete(
- bones.SessionAPI.fromURL(fixture.url))
- self.assertEqual(
- json.loads(description.decode("utf-8")),
- session.description)
-
-
-def load_api_descriptions():
- for name, path in list_api_descriptions():
- description = path.read_text("utf-8")
- yield name, json.loads(description)
-
-
-api_descriptions = list(load_api_descriptions())
-assert len(api_descriptions) != 0
-
-
-class TestActionAPI_APIVersions(TestCase):
- """Tests for `ActionAPI` with multiple API versions."""
-
- scenarios = tuple(
- (name, dict(description=description))
- for name, description in api_descriptions
- )
-
- def test__Version_read(self):
- session = bones.SessionAPI(self.description)
- action = session.Version.read
- self.assertThat(action, MatchesStructure.byEquality(
- name="read", fullname="Version.read", method="GET",
- handler=session.Version, is_restful=True, op=None,
- ))
-
- def test__Machines_deployment_status(self):
- session = bones.SessionAPI(self.description, ("a", "b", "c"))
- action = session.Machines.deployment_status
- self.assertThat(action, MatchesStructure.byEquality(
- name="deployment_status", fullname="Machines.deployment_status",
- method="GET", handler=session.Machines, is_restful=False,
- op="deployment_status",
- ))
-
-
-class TestCallAPI_APIVersions(TestCase):
- """Tests for `CallAPI` with multiple API versions."""
-
- scenarios = tuple(
- (name, dict(description=description))
- for name, description in api_descriptions
- )
-
- 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"))
- call = session.Machines.deployment_status.bind()
- call.dispatch = Mock()
-
- call.call(nodes=system_ids)
-
- call.dispatch.assert_called_once_with(ANY, ANY, ANY)
- uri, body, headers = call.dispatch.call_args[0]
- uri = urlparse(uri)
- self.assertThat(uri.path, Equals("/MAAS/api/2.0/machines/"))
- query_expected = [('op', 'deployment_status')]
- query_expected.extend(('nodes', system_id) for system_id in system_ids)
- query_observed = parse_qsl(uri.query)
- self.assertThat(query_observed, Equals(query_expected))
diff --git a/maas/client/bones/tests/test_helpers.py b/maas/client/bones/tests/test_helpers.py
new file mode 100644
index 00000000..ca740e1f
--- /dev/null
+++ b/maas/client/bones/tests/test_helpers.py
@@ -0,0 +1,423 @@
+"""Tests for `maas.client.bones.helpers`."""
+
+import json
+from unittest.mock import Mock
+from urllib.parse import urlparse, urlsplit
+
+import aiohttp.web
+from macaroonbakery.httpbakery import Client
+from testtools import ExpectedException
+from testtools.matchers import Equals, Is, IsInstance, MatchesDict
+
+from .. import helpers, testing
+from ...testing import AsyncCallableMock, make_name, make_name_without_spaces, TestCase
+from ...utils import api_url, profiles
+from ...utils.testing import make_Credentials
+from ..testing import api_descriptions
+from ..testing.server import ApplicationBuilder
+
+
+class TestFetchAPIDescription(TestCase):
+ """Tests for `fetch_api_description`."""
+
+ def test__raises_RemoteError_when_request_fails(self):
+ fixture = self.useFixture(testing.DescriptionServer(b"bogus"))
+ error = self.assertRaises(
+ helpers.RemoteError,
+ self.loop.run_until_complete,
+ helpers.fetch_api_description(fixture.url + "bogus/"),
+ )
+ self.assertEqual(fixture.url + "bogus/ -> 404 Not Found", str(error))
+
+ def test__raises_RemoteError_when_content_not_json(self):
+ fixture = self.useFixture(testing.DescriptionServer())
+ fixture.handler.content_type = "text/json"
+ error = self.assertRaises(
+ helpers.RemoteError,
+ self.loop.run_until_complete,
+ helpers.fetch_api_description(fixture.url),
+ )
+ self.assertEqual("Expected application/json, got: text/json", str(error))
+
+
+class TestFetchAPIDescriptionURLs(TestCase):
+ """Tests for URL types accepted by `fetch_api_description`."""
+
+ scenarios = (
+ ("string", dict(prepare=str)),
+ ("split", dict(prepare=urlsplit)),
+ ("parsed", dict(prepare=urlparse)),
+ )
+
+ def test__accepts_prepared_url(self):
+ description = {"foo": make_name_without_spaces("bar")}
+ description_json = json.dumps(description).encode("ascii")
+ fixture = self.useFixture(testing.DescriptionServer(description_json))
+ description_url = self.prepare(fixture.url) # Parse, perhaps.
+ description_fetched = self.loop.run_until_complete(
+ helpers.fetch_api_description(description_url)
+ )
+ self.assertThat(description_fetched, Equals(description))
+
+
+class TestFetchAPIDescription_APIVersions(TestCase):
+ """Tests for `fetch_api_description` with multiple API versions."""
+
+ scenarios = tuple(
+ (name, dict(version=version, path=path))
+ for name, version, path in testing.list_api_descriptions()
+ )
+
+ def test__downloads_description(self):
+ description = self.path.read_bytes()
+ fixture = self.useFixture(testing.DescriptionServer(description))
+ description_fetched = self.loop.run_until_complete(
+ helpers.fetch_api_description(fixture.url)
+ )
+ self.assertThat(
+ description_fetched, Equals(json.loads(description.decode("utf-8")))
+ )
+
+
+class TestConnect(TestCase):
+ """Tests for `maas.client.utils.connect.connect`."""
+
+ def setUp(self):
+ super(TestConnect, self).setUp()
+ self.patch(helpers, "fetch_api_description", AsyncCallableMock(return_value={}))
+
+ def test__anonymous(self):
+ # Connect without an apikey.
+ profile = helpers.connect("http://example.org:5240/MAAS/")
+ helpers.fetch_api_description.assert_called_once_with(
+ urlparse("http://example.org:5240/MAAS/api/2.0/"), False
+ )
+ # A Profile instance was returned with no credentials.
+ self.assertThat(profile, IsInstance(profiles.Profile))
+ self.assertThat(profile.credentials, Is(None))
+
+ def test__connected_when_apikey_provided(self):
+ credentials = make_Credentials()
+ # Connect with an apikey.
+ profile = helpers.connect(
+ "http://example.org:5240/MAAS/", apikey=str(credentials)
+ )
+ # The description was fetched.
+ helpers.fetch_api_description.assert_called_once_with(
+ urlparse("http://example.org:5240/MAAS/api/2.0/"), False
+ )
+ # A Profile instance was returned with the expected credentials.
+ self.assertThat(profile, IsInstance(profiles.Profile))
+ self.assertThat(profile.credentials, Equals(credentials))
+
+ def test__complains_when_username_in_URL(self):
+ self.assertRaises(
+ helpers.ConnectError,
+ helpers.connect,
+ "http://foo:bar@example.org:5240/MAAS/",
+ )
+
+ def test__complains_when_password_in_URL(self):
+ self.assertRaises(
+ helpers.ConnectError, helpers.connect, "http://:bar@example.org:5240/MAAS/"
+ )
+
+ def test__URL_is_normalised_to_point_at_API_endpoint(self):
+ profile = helpers.connect("http://example.org:5240/MAAS/")
+ self.assertThat(profile.url, Equals(api_url("http://example.org:5240/MAAS/")))
+
+ def test__profile_is_given_default_name_based_on_URL(self):
+ domain = make_name_without_spaces("domain")
+ profile = helpers.connect("http://%s/MAAS/" % domain)
+ self.assertThat(profile.name, Equals(domain))
+
+ def test__API_description_is_saved_in_profile(self):
+ description = helpers.fetch_api_description.return_value = {"foo": "bar"}
+ profile = helpers.connect("http://example.org:5240/MAAS/")
+ self.assertThat(profile.description, Equals(description))
+
+ def test__API_description_is_fetched_insecurely_if_requested(self):
+ profile = helpers.connect("http://example.org:5240/MAAS/", insecure=True)
+ helpers.fetch_api_description.assert_called_once_with(
+ urlparse("http://example.org:5240/MAAS/api/2.0/"), True
+ )
+ self.assertTrue(profile.other["insecure"])
+
+
+class TestLogin(TestCase):
+ """Tests for `maas.client.utils.login.login`."""
+
+ def setUp(self):
+ super(TestLogin, self).setUp()
+ self.patch(helpers, "authenticate", AsyncCallableMock(return_value=None))
+ self.patch(helpers, "fetch_api_description", AsyncCallableMock(return_value={}))
+
+ def test__anonymous(self):
+ # Log-in anonymously.
+ profile = helpers.login("http://example.org:5240/MAAS/", anonymous=True)
+ # No token was obtained, but the description was fetched.
+ helpers.authenticate.assert_not_called()
+ # A Profile instance was returned with no credentials.
+ self.assertThat(profile, IsInstance(profiles.Profile))
+ self.assertThat(profile.credentials, Is(None))
+
+ def test__macaroon_auth_with_no_username_and_password(self):
+ credentials = make_Credentials()
+ self.patch(
+ helpers,
+ "authenticate_with_macaroon",
+ AsyncCallableMock(return_value=credentials),
+ )
+ # Log-in without a user-name or a password.
+ profile = helpers.login("http://example.org:5240/MAAS/")
+ # A token is obtained via macaroons, but the description was fetched.
+ # The description was fetched.
+ helpers.fetch_api_description.assert_called_once_with(
+ urlparse("http://example.org:5240/MAAS/api/2.0/"), False
+ )
+ # The returned profile uses credentials obtained from the
+ # authentication
+ self.assertThat(profile, IsInstance(profiles.Profile))
+ self.assertThat(profile.credentials, Is(credentials))
+
+ def test__authenticated_when_username_and_password_provided(self):
+ credentials = make_Credentials()
+ helpers.authenticate.return_value = credentials
+ # Log-in with a user-name and a password.
+ profile = helpers.login("http://foo:bar@example.org:5240/MAAS/")
+ # A token was obtained, and the description was fetched.
+ helpers.authenticate.assert_called_once_with(
+ "http://example.org:5240/MAAS/api/2.0/", "foo", "bar", insecure=False
+ )
+ # A Profile instance was returned with the expected credentials.
+ self.assertThat(profile, IsInstance(profiles.Profile))
+ self.assertThat(profile.credentials, Is(credentials))
+
+ def test__complains_when_username_but_not_password(self):
+ self.assertRaises(
+ helpers.UsernameWithoutPassword,
+ helpers.login,
+ "http://example.org:5240/MAAS/",
+ username="alice",
+ )
+
+ def test__complains_when_password_but_not_username(self):
+ self.assertRaises(
+ helpers.PasswordWithoutUsername,
+ helpers.login,
+ "http://example.org:5240/MAAS/",
+ password="wonderland",
+ )
+
+ def test__complains_when_username_in_URL_and_passed_explicitly(self):
+ self.assertRaises(
+ helpers.LoginError,
+ helpers.login,
+ "http://foo:bar@example.org:5240/MAAS/",
+ username="alice",
+ )
+
+ def test__complains_when_empty_username_in_URL_and_passed_explicitly(self):
+ self.assertRaises(
+ helpers.LoginError,
+ helpers.login,
+ "http://:bar@example.org:5240/MAAS/",
+ username="alice",
+ )
+
+ def test__complains_when_password_in_URL_and_passed_explicitly(self):
+ self.assertRaises(
+ helpers.LoginError,
+ helpers.login,
+ "http://foo:bar@example.org:5240/MAAS/",
+ password="wonderland",
+ )
+
+ def test__complains_when_empty_password_in_URL_and_passed_explicitly(self):
+ self.assertRaises(
+ helpers.LoginError,
+ helpers.login,
+ "http://foo:@example.org:5240/MAAS/",
+ password="wonderland",
+ )
+
+ def test__URL_is_normalised_to_point_at_API_endpoint(self):
+ profile = helpers.login("http://example.org:5240/MAAS/", anonymous=True)
+ self.assertThat(profile.url, Equals(api_url("http://example.org:5240/MAAS/")))
+
+ def test__profile_is_given_default_name_based_on_URL(self):
+ domain = make_name_without_spaces("domain")
+ profile = helpers.login("http://%s/MAAS/" % domain, anonymous=True)
+ self.assertThat(profile.name, Equals(domain))
+
+ def test__API_description_is_saved_in_profile(self):
+ description = {make_name("key"): make_name("value")}
+ helpers.fetch_api_description.return_value = description
+ profile = helpers.login("http://example.org:5240/MAAS/", anonymous=True)
+ self.assertThat(profile.description, Equals(description))
+
+ def test__API_token_is_fetched_insecurely_if_requested(self):
+ profile = helpers.login("http://foo:bar@example.org:5240/MAAS/", insecure=True)
+ helpers.authenticate.assert_called_once_with(
+ "http://example.org:5240/MAAS/api/2.0/", "foo", "bar", insecure=True
+ )
+ self.assertTrue(profile.other["insecure"])
+
+ def test__API_description_is_fetched_insecurely_if_requested(self):
+ helpers.login("http://example.org:5240/MAAS/", anonymous=True, insecure=True)
+ helpers.fetch_api_description.assert_called_once_with(
+ urlparse("http://example.org:5240/MAAS/api/2.0/"), True
+ )
+
+ def test__uses_username_from_URL_if_set(self):
+ helpers.login("http://foo@maas.io/", password="bar")
+ helpers.authenticate.assert_called_once_with(
+ "http://maas.io/api/2.0/", "foo", "bar", insecure=False
+ )
+
+ def test__uses_username_and_password_from_URL_if_set(self):
+ helpers.login("http://foo:bar@maas.io/")
+ helpers.authenticate.assert_called_once_with(
+ "http://maas.io/api/2.0/", "foo", "bar", insecure=False
+ )
+
+ def test__uses_empty_username_and_password_in_URL_if_set(self):
+ helpers.login("http://:@maas.io/")
+ helpers.authenticate.assert_called_once_with(
+ "http://maas.io/api/2.0/", "", "", insecure=False
+ )
+
+
+class TestAuthenticate(TestCase):
+ """Tests for `authenticate`."""
+
+ scenarios = tuple(
+ (name, dict(version=version, description=description))
+ for name, version, description in api_descriptions
+ )
+
+ async def test__obtains_credentials_from_server(self):
+ builder = ApplicationBuilder(self.description)
+
+ @builder.handle("anon:Version.read")
+ async def version(request):
+ return {"capabilities": ["authenticate-api"]}
+
+ credentials = make_Credentials()
+ parameters = None
+
+ @builder.route("POST", "/accounts/authenticate/")
+ async def deploy(request):
+ nonlocal parameters
+ parameters = await request.post()
+ return {
+ "consumer_key": credentials.consumer_key,
+ "token_key": credentials.token_key,
+ "token_secret": credentials.token_secret,
+ }
+
+ username = make_name_without_spaces("username")
+ password = make_name_without_spaces("password")
+
+ async with builder.serve() as baseurl:
+ credentials_observed = await helpers.authenticate(
+ baseurl, username, password
+ )
+
+ self.assertThat(credentials_observed, Equals(credentials))
+ self.assertThat(
+ parameters,
+ MatchesDict(
+ {
+ "username": Equals(username),
+ "password": Equals(password),
+ "consumer": IsInstance(str),
+ }
+ ),
+ )
+
+ async def test__raises_error_when_server_does_not_support_authn(self):
+ builder = ApplicationBuilder(self.description)
+
+ @builder.handle("anon:Version.read")
+ async def version(request):
+ return {"capabilities": []}
+
+ async with builder.serve() as baseurl:
+ with ExpectedException(helpers.LoginNotSupported):
+ await helpers.authenticate(baseurl, "username", "password")
+
+ async def test__raises_error_when_server_rejects_credentials(self):
+ builder = ApplicationBuilder(self.description)
+
+ @builder.handle("anon:Version.read")
+ async def version(request):
+ return {"capabilities": ["authenticate-api"]}
+
+ @builder.route("POST", "/accounts/authenticate/")
+ async def deploy(request):
+ raise aiohttp.web.HTTPForbidden()
+
+ async with builder.serve() as baseurl:
+ with ExpectedException(helpers.RemoteError):
+ await helpers.authenticate(baseurl, "username", "password")
+
+
+class TestAuthenticateWithMacaroon(TestCase):
+ def setUp(self):
+ super().setUp()
+ self.mock_client_request = self.patch(Client, "request")
+ self.token_result = {
+ "consumer_key": "abc",
+ "token_key": "123",
+ "token_secret": "xyz",
+ }
+ self.mock_response = Mock()
+ self.mock_response.status_code = 200
+ self.mock_response.json.return_value = self.token_result
+ self.mock_client_request.return_value = self.mock_response
+
+ async def test__authenticate_with_bakery_creates_token(self):
+ credentials = await helpers.authenticate_with_macaroon("http://example.com")
+ self.assertEqual(credentials, "abc:123:xyz")
+ # a call to create an API token is made
+ self.mock_client_request.assert_called_once_with(
+ "POST",
+ "http://example.com/account/?op=create_authorisation_token",
+ verify=True,
+ )
+
+ async def test__authenticate_failed_request(self):
+ self.mock_response.status_code = 500
+ self.mock_response.text = "error!"
+ try:
+ await helpers.authenticate_with_macaroon("http://example.com")
+ except helpers.LoginError as e:
+ self.assertEqual(str(e), "Login failed: error!")
+ else:
+ self.fail("LoginError not raised")
+
+ async def test__authenticate_macaroon_not_supported(self):
+ self.mock_response.status_code = 401
+ try:
+ await helpers.authenticate_with_macaroon("http://example.com")
+ except helpers.MacaroonLoginNotSupported as e:
+ self.assertEqual(str(e), "Macaroon authentication not supported")
+ else:
+ self.fail("MacaroonLoginNotSupported not raised")
+
+
+class TestDeriveResourceName(TestCase):
+ """Tests for `derive_resource_name`."""
+
+ def test__removes_Anon_prefix(self):
+ self.assertThat(helpers.derive_resource_name("AnonFooBar"), Equals("FooBar"))
+
+ def test__removes_Handler_suffix(self):
+ self.assertThat(helpers.derive_resource_name("FooBarHandler"), Equals("FooBar"))
+
+ def test__normalises_Maas_to_MAAS(self):
+ self.assertThat(helpers.derive_resource_name("Maas"), Equals("MAAS"))
+
+ def test__does_all_the_above(self):
+ self.assertThat(helpers.derive_resource_name("AnonMaasHandler"), Equals("MAAS"))
diff --git a/maas/client/enum.py b/maas/client/enum.py
new file mode 100644
index 00000000..3da12cfb
--- /dev/null
+++ b/maas/client/enum.py
@@ -0,0 +1,164 @@
+__all__ = ["NodeStatus"]
+
+import enum
+
+
+class NodeStatus(enum.IntEnum):
+ #: A node starts out as NEW (DEFAULT is an alias for NEW).
+ DEFAULT = 0
+ #: The node has been created and has a system ID assigned to it.
+ NEW = 0
+ #: Testing and other commissioning steps are taking place.
+ COMMISSIONING = 1
+ #: The commissioning step failed.
+ FAILED_COMMISSIONING = 2
+ #: The node can't be contacted.
+ MISSING = 3
+ #: The node is in the general pool ready to be deployed.
+ READY = 4
+ #: The node is ready for named deployment.
+ RESERVED = 5
+ #: The node has booted into the operating system of its owner's choice
+ #: and is ready for use.
+ DEPLOYED = 6
+ #: The node has been removed from service manually until an admin
+ #: overrides the retirement.
+ RETIRED = 7
+ #: The node is broken: a step in the node lifecyle failed.
+ #: More details can be found in the node's event log.
+ BROKEN = 8
+ #: The node is being installed.
+ DEPLOYING = 9
+ #: The node has been allocated to a user and is ready for deployment.
+ ALLOCATED = 10
+ #: The deployment of the node failed.
+ FAILED_DEPLOYMENT = 11
+ #: The node is powering down after a release request.
+ RELEASING = 12
+ #: The releasing of the node failed.
+ FAILED_RELEASING = 13
+ #: The node is erasing its disks.
+ DISK_ERASING = 14
+ #: The node failed to erase its disks.
+ FAILED_DISK_ERASING = 15
+ #: The node is in rescue mode.
+ RESCUE_MODE = 16
+ #: The node is entering rescue mode.
+ ENTERING_RESCUE_MODE = 17
+ #: The node failed to enter rescue mode.
+ FAILED_ENTERING_RESCUE_MODE = 18
+ #: The node is exiting rescue mode.
+ EXITING_RESCUE_MODE = 19
+ #: The node failed to exit rescue mode.
+ FAILED_EXITING_RESCUE_MODE = 20
+ #: Running tests on Node
+ TESTING = 21
+ #: Testing has failed
+ FAILED_TESTING = 22
+
+
+class NodeType(enum.IntEnum):
+ #: Machine
+ MACHINE = 0
+ #: Device
+ DEVICE = 1
+ #: Rack
+ RACK_CONTROLLER = 2
+ #: Region
+ REGION_CONTROLLER = 3
+ #: Region+Rack
+ REGION_AND_RACK_CONTROLLER = 4
+
+
+class PowerState(enum.Enum):
+ #: On
+ ON = "on"
+ #: Off
+ OFF = "off"
+ #: Unknown
+ UNKNOWN = "unknown"
+ #: Error
+ ERROR = "error"
+
+
+class PowerStopMode(enum.Enum):
+ #: Perform hard stop.
+ HARD = "hard"
+ #: Perform soft stop.
+ SOFT = "soft"
+
+
+class RDNSMode(enum.IntEnum):
+ #: Do not generate reverse DNS for this Subnet.
+ DISABLED = 0
+ #: Generate reverse DNS only for the CIDR.
+ ENABLED = 1
+ #: Generate RFC2317 glue if needed (Subnet is too small for its own zone.)
+ RFC2317 = 2
+
+
+class IPRangeType(enum.Enum):
+ #: Dynamic IP Range.
+ DYNAMIC = "dynamic"
+ #: Reserved for exclusive use by MAAS or user.
+ RESERVED = "reserved"
+
+
+class InterfaceType(enum.Enum):
+ #: Physical interface.
+ PHYSICAL = "physical"
+ #: Bonded interface.
+ BOND = "bond"
+ #: Bridge interface.
+ BRIDGE = "bridge"
+ #: VLAN interface.
+ VLAN = "vlan"
+ #: Interface not linked to a node.
+ UNKNOWN = "unknown"
+
+
+class LinkMode(enum.Enum):
+ #: IP is auto assigned by MAAS.
+ AUTO = "auto"
+ #: IP is assigned by a DHCP server.
+ DHCP = "dhcp"
+ #: IP is statically assigned.
+ STATIC = "static"
+ #: Connected to subnet with no IP address.
+ LINK_UP = "link_up"
+
+
+class BlockDeviceType(enum.Enum):
+ #: Physical block device.
+ PHYSICAL = "physical"
+ #: Virtual block device.
+ VIRTUAL = "virtual"
+
+
+class PartitionTableType(enum.Enum):
+ #: Master boot record
+ MBR = "mbr"
+ #: GUID Partition Table
+ GPT = "gpt"
+
+
+class RaidLevel(enum.Enum):
+ #: RAID level 0
+ RAID_0 = "raid-0"
+ #: RAID level 1
+ RAID_1 = "raid-1"
+ #: RAID level 5
+ RAID_5 = "raid-5"
+ #: RAID level 6
+ RAID_6 = "raid-6"
+ #: RAID level 10
+ RAID_10 = "raid-10"
+
+
+class CacheMode(enum.Enum):
+ #: Writeback
+ WRITEBACK = "writeback"
+ #: Writethough
+ WRITETHROUGH = "writethrough"
+ #: Writearound
+ WRITEAROUND = "writearound"
diff --git a/maas/client/errors.py b/maas/client/errors.py
new file mode 100644
index 00000000..963339ff
--- /dev/null
+++ b/maas/client/errors.py
@@ -0,0 +1,25 @@
+""" Custom errors for libmaas """
+
+__all__ = ["MAASException", "OperationNotAllowed"]
+
+
+class MAASException(Exception):
+ def __init__(self, msg, obj):
+ super().__init__(msg)
+ self.obj = obj
+
+
+class OperationNotAllowed(Exception):
+ """MAAS says this operation cannot be performed."""
+
+
+class ObjectNotLoaded(Exception):
+ """Object is not loaded."""
+
+
+class CannotDelete(Exception):
+ """Object cannot be deleted."""
+
+
+class PowerError(MAASException):
+ """Machine failed to power on or off."""
diff --git a/maas/client/facade.py b/maas/client/facade.py
new file mode 100644
index 00000000..0a8db25d
--- /dev/null
+++ b/maas/client/facade.py
@@ -0,0 +1,271 @@
+"""Client facade."""
+
+import enum
+from functools import update_wrapper
+
+
+class Facade:
+ """Present a simplified API for interacting with MAAS.
+
+ The viscera API separates set-based interactions from those on individual
+ objects — e.g. Machines and Machine — which mirrors the way MAAS's API is
+ actually constructed, helps to avoid namespace clashes, and makes testing
+ cleaner.
+
+ However, we want to present a simplified commingled namespace to users of
+ MAAS's *client* API. For example, all entry points related to machines
+ should be available as ``client.machines``. This facade class allows us to
+ present that commingled namespace without coding it as such.
+ """
+
+ def __init__(self, client, name, methods):
+ super(Facade, self).__init__()
+ self._client = client
+ self._name = name
+ self._populate(methods)
+
+ def _populate(self, methods):
+ for name, func in methods.items():
+ setattr(self, name, func)
+
+ def __repr__(self):
+ return "<%s>" % self._name
+
+
+class FacadeDescriptor:
+ """Lazily create a facade on first use.
+
+ It will be stored in the instance dictionary using the given name. This
+ should match the name by which the descriptor is bound into the instance
+ class's namespace: as this is a non-data descriptor [1] this will yield
+ create-on-first-use behaviour.
+
+ The factory function should accept a single argument, an `Origin`, and
+ return a dict mapping method names to methods of objects obtained from the
+ origin.
+
+ [1] https://docs.python.org/3.5/howto/descriptor.html#descriptor-protocol
+ """
+
+ def __init__(self, name, factory):
+ super(FacadeDescriptor, self).__init__()
+ self.name, self.factory = name, factory
+
+ def __get__(self, obj, typ=None):
+ methods = self.factory(obj._origin)
+ facade = Facade(obj, self.name, methods)
+ obj.__dict__[self.name] = facade
+ return facade
+
+
+def facade(factory):
+ """Declare a method as a facade factory."""
+ wrapper = FacadeDescriptor(factory.__name__, factory)
+ return update_wrapper(wrapper, factory)
+
+
+class Client:
+ """A simplified API for interacting with MAAS."""
+
+ def __init__(self, origin):
+ super(Client, self).__init__()
+ self._origin = origin
+
+ @facade
+ def account(origin):
+ return {
+ "create_credentials": origin.Account.create_credentials,
+ "delete_credentials": origin.Account.delete_credentials,
+ }
+
+ @facade
+ def boot_resources(origin):
+ return {
+ "create": origin.BootResources.create,
+ "get": origin.BootResource.read,
+ "list": origin.BootResources.read,
+ "start_import": origin.BootResources.start_import,
+ "stop_import": origin.BootResources.stop_import,
+ }
+
+ @facade
+ def boot_sources(origin):
+ return {
+ "create": origin.BootSources.create,
+ "get": origin.BootSource.read,
+ "list": origin.BootSources.read,
+ }
+
+ @facade
+ def devices(origin):
+ return {
+ "create": origin.Devices.create,
+ "get": origin.Device.read,
+ "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 {
+ "create": origin.Domains.create,
+ "get": origin.Domain.read,
+ "list": origin.Domains.read,
+ }
+
+ @facade
+ def events(origin):
+ namespace = {"query": origin.Events.query}
+ namespace.update({level.name: level for level in origin.Events.Level})
+ return namespace
+
+ @facade
+ def fabrics(origin):
+ return {
+ "create": origin.Fabrics.create,
+ "get": origin.Fabric.read,
+ "get_default": origin.Fabric.get_default,
+ "list": origin.Fabrics.read,
+ }
+
+ @facade
+ def pods(origin):
+ return {
+ "create": origin.Pods.create,
+ "list": origin.Pods.read,
+ "get": origin.Pod.read,
+ }
+
+ @facade
+ def static_routes(origin):
+ return {
+ "create": origin.StaticRoutes.create,
+ "get": origin.StaticRoute.read,
+ "list": origin.StaticRoutes.read,
+ }
+
+ @facade
+ def subnets(origin):
+ return {
+ "create": origin.Subnets.create,
+ "get": origin.Subnet.read,
+ "list": origin.Subnets.read,
+ }
+
+ @facade
+ def spaces(origin):
+ return {
+ "create": origin.Spaces.create,
+ "get": origin.Space.read,
+ "get_default": origin.Space.get_default,
+ "list": origin.Spaces.read,
+ }
+
+ @facade
+ def files(origin):
+ return {"list": origin.Files.read}
+
+ @facade
+ def ip_ranges(origin):
+ return {
+ "create": origin.IPRanges.create,
+ "get": origin.IPRange.read,
+ "list": origin.IPRanges.read,
+ }
+
+ @facade
+ def ip_addresses(origin):
+ return {
+ "list": origin.IPAddresses.read,
+ }
+
+ @facade
+ def maas(origin):
+ attrs = (
+ (name, getattr(origin.MAAS, name))
+ for name in dir(origin.MAAS)
+ if not name.startswith("_")
+ )
+ return {
+ name: attr
+ for name, attr in attrs
+ if isinstance(attr, enum.EnumMeta) or name.startswith(("get_", "set_"))
+ }
+
+ @facade
+ def machines(origin):
+ return {
+ "allocate": origin.Machines.allocate,
+ "create": origin.Machines.create,
+ "get": origin.Machine.read,
+ "list": origin.Machines.read,
+ "get_power_parameters_for": origin.Machines.get_power_parameters_for,
+ }
+
+ @facade
+ def rack_controllers(origin):
+ return {"get": origin.RackController.read, "list": origin.RackControllers.read}
+
+ @facade
+ def region_controllers(origin):
+ return {
+ "get": origin.RegionController.read,
+ "list": origin.RegionControllers.read,
+ }
+
+ @facade
+ def ssh_keys(origin):
+ return {
+ "create": origin.SSHKeys.create,
+ "get": origin.SSHKey.read,
+ "list": origin.SSHKeys.read,
+ }
+
+ @facade
+ def tags(origin):
+ return {
+ "create": origin.Tags.create,
+ "get": origin.Tag.read,
+ "list": origin.Tags.read,
+ }
+
+ @facade
+ def users(origin):
+ return {
+ "create": origin.Users.create,
+ "list": origin.Users.read,
+ "whoami": origin.Users.whoami,
+ }
+
+ @facade
+ def version(origin):
+ return {"get": origin.Version.read}
+
+ @facade
+ def zones(origin):
+ return {
+ "create": origin.Zones.create,
+ "get": origin.Zone.read,
+ "list": origin.Zones.read,
+ }
+
+ @facade
+ def resource_pools(origin):
+ return {
+ "create": origin.ResourcePools.create,
+ "get": origin.ResourcePool.read,
+ "list": origin.ResourcePools.read,
+ }
diff --git a/maas/client/flesh/__init__.py b/maas/client/flesh/__init__.py
index 17ef7a9c..7c1fa644 100644
--- a/maas/client/flesh/__init__.py
+++ b/maas/client/flesh/__init__.py
@@ -11,32 +11,46 @@
"TableCommand",
]
-from abc import (
- ABCMeta,
- abstractmethod,
-)
+from abc import ABCMeta, abstractmethod
import argparse
from importlib import import_module
+import os
+import subprocess
import sys
-from typing import (
- Optional,
- Sequence,
- Tuple,
-)
+import textwrap
+import typing
import argcomplete
import colorclass
from . import tabular
-from .. import (
- bones,
- utils,
- viscera,
-)
-from ..utils.profiles import (
- Profile,
- ProfileStore,
-)
+from .. import bones, utils, viscera
+from ..utils.auth import try_getpass
+from ..utils.profiles import Profile, ProfileStore
+
+
+PROG_DESCRIPTION = """\
+MAAS provides complete automation of your physical servers for amazing data
+center operational efficiency.
+
+See https://maas.io/docs for documentation.
+
+Common commands:
+
+ {program} login Log-in to a MAAS.
+ {program} switch Switch the active profile.
+ {program} machines List machines.
+ {program} deploy Allocate and deploy machine.
+ {program} release Release machine.
+ {program} fabrics List fabrics.
+ {program} subnets List subnets.
+
+Example help commands:
+
+ `{program} help` This help page
+ `{program} help commands` Lists all commands
+ `{program} help deploy` Shows help for command 'deploy'
+"""
def colorized(text):
@@ -48,7 +62,61 @@ def colorized(text):
return colorclass.Color(text).value_no_colors
-def get_profile_names_and_default() -> Tuple[Sequence[str], Optional[Profile]]:
+def read_input(message, validator=None, password=False):
+ message = "%s: " % message
+ while True:
+ if password:
+ value = try_getpass(message)
+ else:
+ value = input(message)
+ if value:
+ if validator is not None:
+ try:
+ validator(value)
+ except Exception as exc:
+ print(colorized("{{autored}}Error: {{/autored}} %s") % str(exc))
+ else:
+ return value
+ else:
+ return value
+
+
+def yes_or_no(question):
+ question = "%s [y/N] " % question
+ while True:
+ value = input(question)
+ value = value.lower()
+ if value in ["y", "yes"]:
+ return True
+ elif value in ["n", "no"]:
+ return False
+
+
+def print_with_pager(output):
+ """Print the output to `stdout` using less when in a tty."""
+ if sys.stdout.isatty():
+ try:
+ pager = subprocess.Popen(
+ ["less", "-F", "-r", "-S", "-X", "-K"],
+ stdin=subprocess.PIPE,
+ stdout=sys.stdout,
+ )
+ except subprocess.CalledProcessError:
+ # Don't use the pager since starting it has failed.
+ print(output)
+ return
+ else:
+ pager.stdin.write(output.encode("utf-8"))
+ pager.stdin.close()
+ pager.wait()
+ else:
+ # Output directly to stdout since not in tty.
+ print(output)
+
+
+def get_profile_names_and_default() -> (
+ typing.Tuple[typing.Sequence[str], typing.Optional[Profile]]
+):
"""Return the list of profile names and the default profile object.
The list of names is sorted.
@@ -63,16 +131,42 @@ def get_profile_names_and_default() -> Tuple[Sequence[str], Optional[Profile]]:
PROFILE_NAMES, PROFILE_DEFAULT = get_profile_names_and_default()
+class MinimalHelpAction(argparse._HelpAction):
+ def __call__(self, parser, namespace, values, option_string=None):
+ parser.print_minized_help()
+ parser.exit()
+
+
+class PagedHelpAction(argparse._HelpAction):
+ def __call__(self, parser, namespace, values, option_string=None):
+ print_with_pager(parser.format_help())
+ parser.exit()
+
+
+class HelpFormatter(argparse.RawDescriptionHelpFormatter):
+ """Specialization of argparse's raw description help formatter to modify
+ usage to be in a better format.
+ """
+
+ def _format_usage(self, usage, actions, groups, prefix):
+ if prefix is None:
+ prefix = "Usage: "
+ return super(HelpFormatter, self)._format_usage(usage, actions, groups, prefix)
+
+
class ArgumentParser(argparse.ArgumentParser):
- """Specialisation of argparse's parser with better support for subparsers.
+ """Specialization of argparse's parser with better support for
+ subparsers and better help output.
Specifically, the one-shot `add_subparsers` call is disabled, replaced by
a lazily evaluated `subparsers` property.
+
+ `print_minized_help` is added to only show the description which is
+ specially formatted.
"""
def add_subparsers(self):
- raise NotImplementedError(
- "add_subparsers has been disabled")
+ raise NotImplementedError("add_subparsers has been disabled")
@property
def subparsers(self):
@@ -94,7 +188,8 @@ def add_argument_group(self, title, description=None):
if title not in groups:
groups[title] = super().add_argument_group(
- title=title, description=description)
+ title=title, description=description
+ )
return groups[title]
@@ -115,6 +210,20 @@ def error(self, message):
"""
self.exit(2, colorized("{autored}Error:{/autored} ") + message + "\n")
+ def print_minized_help(self, *, no_pager=False):
+ """Return the formatted help text.
+
+ Override default ArgumentParser to just include the usage and the
+ description. The `help` action is used for provide more detail.
+ """
+ formatter = self._get_formatter()
+ formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
+ formatter.add_text(self.description)
+ if no_pager:
+ print(formatter.format_help())
+ else:
+ print_with_pager(formatter.format_help())
+
class CommandError(Exception):
"""A command has failed during execution."""
@@ -150,16 +259,21 @@ def register(cls, parser, name=None):
"""
help_title, help_body = utils.parse_docstring(cls)
command_parser = parser.subparsers.add_parser(
- cls.name() if name is None else name, help=help_title,
- description=help_title, epilog=help_body, add_help=False)
+ cls.name() if name is None else name,
+ help=help_title,
+ description=help_title,
+ epilog=help_body,
+ add_help=False,
+ formatter_class=HelpFormatter,
+ )
command_parser.add_argument(
- "-h", "--help", action="help", help=argparse.SUPPRESS)
+ "-h", "--help", action=PagedHelpAction, help=argparse.SUPPRESS
+ )
command_parser.set_defaults(execute=cls(command_parser))
return command_parser
class TableCommand(Command):
-
def __init__(self, parser):
super(TableCommand, self).__init__(parser)
if sys.stdout.isatty():
@@ -167,8 +281,11 @@ def __init__(self, parser):
else:
default_target = tabular.RenderTarget.plain
parser.other.add_argument(
- "--format", type=tabular.RenderTarget,
- choices=tabular.RenderTarget, default=default_target, help=(
+ "--format",
+ type=tabular.RenderTarget,
+ choices=tabular.RenderTarget,
+ default=default_target,
+ help=(
"Output tabular data as a formatted table (pretty), a "
"formatted table using only ASCII for borders (plain), or "
"one of several dump formats. Default: %(default)s."
@@ -177,71 +294,198 @@ def __init__(self, parser):
class OriginCommandBase(Command):
-
def __init__(self, parser):
super(OriginCommandBase, self).__init__(parser)
parser.other.add_argument(
- "--profile", dest="profile_name", metavar="NAME",
- choices=PROFILE_NAMES, required=(PROFILE_DEFAULT is None),
+ "--profile",
+ dest="profile_name",
+ metavar="NAME",
+ choices=PROFILE_NAMES,
+ required=(PROFILE_DEFAULT is None),
help=(
"The name of the remote MAAS instance to use. Use "
- "`profiles list` to obtain a list of valid profiles." +
- ("" if PROFILE_DEFAULT is None else " [default: %(default)s]")
- ))
+ "`profiles list` to obtain a list of valid profiles."
+ + (
+ ""
+ if PROFILE_DEFAULT is None
+ else " [default: %s]" % PROFILE_DEFAULT.name
+ )
+ ),
+ )
if PROFILE_DEFAULT is not None:
parser.set_defaults(profile=PROFILE_DEFAULT.name)
class OriginCommand(OriginCommandBase):
-
def __call__(self, options):
session = bones.SessionAPI.fromProfileName(options.profile)
origin = viscera.Origin(session)
return self.execute(origin, options)
- def execute(self, options, origin):
- raise NotImplementedError(
- "Implement execute() in subclasses.")
+ def execute(self, origin, options):
+ raise NotImplementedError("Implement execute() in subclasses.")
class OriginTableCommand(OriginCommandBase, TableCommand):
-
def __call__(self, options):
session = bones.SessionAPI.fromProfileName(options.profile)
origin = viscera.Origin(session)
return self.execute(origin, options, target=options.format)
- def execute(self, options, origin, *, target):
- raise NotImplementedError(
- "Implement execute() in subclasses.")
+ def execute(self, origin, options, *, target):
+ raise NotImplementedError("Implement execute() in subclasses.")
+
+
+class OriginPagedTableCommand(OriginTableCommand):
+ def __init__(self, parser):
+ super(OriginPagedTableCommand, self).__init__(parser)
+ parser.other.add_argument(
+ "--no-pager",
+ action="store_true",
+ help=("Don't use the pager when printing the output of the " "command."),
+ )
+
+ def __call__(self, options):
+ return_code = 0
+ output = super(OriginPagedTableCommand, self).__call__(options)
+ if isinstance(output, tuple):
+ return_code, output = output
+ elif isinstance(output, int):
+ return_code = output
+ output = None
+ elif isinstance(output, str):
+ pass
+ else:
+ raise TypeError(
+ "execute must return either tuple, int or str, not %s"
+ % (type(output).__name__)
+ )
+ if output:
+ if options.no_pager:
+ print(output)
+ else:
+ print_with_pager(output)
+ return return_code
+
+
+class cmd_help(Command):
+ """Show the help summary or help for a specific command."""
+
+ def __init__(self, parser, parent_parser):
+ self.parent_parser = parent_parser
+ super(cmd_help, self).__init__(parser)
+ parser.add_argument(
+ "-h", "--help", action=PagedHelpAction, help=argparse.SUPPRESS
+ )
+ parser.add_argument("command", nargs="?", help="Show help for this command.")
+ parser.other.add_argument(
+ "--no-pager",
+ action="store_true",
+ help=("Don't use the pager when printing the output of the " "command."),
+ )
+
+ def __call__(self, options):
+ if options.command is None:
+ self.parent_parser.print_minized_help(no_pager=options.no_pager)
+ else:
+ command = self.parent_parser.subparsers.choices.get(options.command, None)
+ if command is None:
+ if options.command == "commands":
+ self.print_all_commands(no_pager=options.no_pager)
+ else:
+ self.parser.error("unknown command %s" % options.command)
+ else:
+ if options.no_pager:
+ command.print_help()
+ else:
+ print_with_pager(command.format_help())
+
+ def print_all_commands(self, *, no_pager=False):
+ """Print help for all commands.
+
+ Commands are sorted in alphabetical order and wrapping is done
+ based on the width of the terminal.
+ """
+ formatter = self.parent_parser._get_formatter()
+ command_names = sorted(self.parent_parser.subparsers.choices.keys())
+ max_name_len = max([len(name) for name in command_names]) + 1
+ commands = ""
+ for name in command_names:
+ command = self.parent_parser.subparsers.choices[name]
+ extra_padding = max_name_len - len(name)
+ command_line = "%s%s%s" % (name, " " * extra_padding, command.description)
+ while len(command_line) > formatter._width:
+ lines = textwrap.wrap(command_line, formatter._width)
+ commands += "%s\n" % lines[0]
+ if len(lines) > 1:
+ lines[1] = (" " * max_name_len) + lines[1]
+ command_line = " ".join(lines[1:])
+ else:
+ command_line = None
+ if command_line:
+ commands += "%s\n" % command_line
+ if no_pager:
+ print(commands[:-1])
+ else:
+ print_with_pager(commands[:-1])
+
+ @classmethod
+ def register(cls, parser, name=None):
+ """Register this command as a sub-parser of `parser`.
+
+ :type parser: An instance of `ArgumentParser`.
+ :return: The sub-parser created.
+ """
+ help_title, help_body = utils.parse_docstring(cls)
+ command_parser = parser.subparsers.add_parser(
+ cls.name() if name is None else name,
+ help=help_title,
+ description=help_title,
+ epilog=help_body,
+ add_help=False,
+ formatter_class=HelpFormatter,
+ )
+ command_parser.set_defaults(execute=cls(command_parser, parser))
+ return command_parser
def prepare_parser(program):
"""Create and populate an argument parser."""
parser = ArgumentParser(
- description="Interact with a remote MAAS server.", prog=program,
- epilog=colorized("If in doubt, try {autogreen}login{/autogreen}."),
- add_help=False)
- parser.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS)
+ description=PROG_DESCRIPTION.format(program=program),
+ prog=program,
+ formatter_class=HelpFormatter,
+ add_help=False,
+ )
+ parser.add_argument(
+ "-h", "--help", action=MinimalHelpAction, help=argparse.SUPPRESS
+ )
# Register sub-commands.
submodules = (
- # These modules are expected to register verb-like commands into the
- # sub-parsers created above, e.g. for "list files", "launch node".
- # Nodes always come first.
- "nodes", "files", "tags", "users",
- # These modules are different: they are collections of commands around
- # a topic, or miscellaneous conveniences.
- "profiles", "shell",
+ "nodes",
+ "machines",
+ "devices",
+ "controllers",
+ "fabrics",
+ "vlans",
+ "subnets",
+ "spaces",
+ "files",
+ "tags",
+ "users",
+ "profiles",
+ "shell",
)
+ cmd_help.register(parser)
for submodule in submodules:
module = import_module("." + submodule, __name__)
module.register(parser)
# Register global options.
parser.add_argument(
- '--debug', action='store_true', default=False,
- help=argparse.SUPPRESS)
+ "--debug", action="store_true", default=False, help=argparse.SUPPRESS
+ )
return parser
@@ -268,8 +512,16 @@ def post_mortem(traceback):
post_mortem(traceback)
+def program_name_from_env(program):
+ """Return the program name from environment."""
+ if os.environ.get("SNAP_INSTANCE_NAME"):
+ return os.environ.get("SNAP_INSTANCE_NAME")
+ return program
+
+
def main(argv=sys.argv):
program, *arguments = argv
+ program = program_name_from_env(program)
parser, options = None, None
try:
@@ -281,7 +533,7 @@ def main(argv=sys.argv):
except AttributeError:
parser.error("Argument missing.")
else:
- execute(options)
+ return execute(options)
except KeyboardInterrupt:
raise SystemExit(1)
except Exception as error:
diff --git a/maas/client/flesh/controllers.py b/maas/client/flesh/controllers.py
new file mode 100644
index 00000000..f93b799f
--- /dev/null
+++ b/maas/client/flesh/controllers.py
@@ -0,0 +1,67 @@
+"""Commands for controllers."""
+
+__all__ = ["register"]
+
+import asyncio
+from itertools import chain
+
+from . import CommandError, OriginPagedTableCommand, tables
+from ..enum import NodeType
+from ..utils.maas_async import asynchronous
+
+
+class cmd_controllers(OriginPagedTableCommand):
+ """List controllers."""
+
+ def __init__(self, parser):
+ super(cmd_controllers, self).__init__(parser)
+ parser.add_argument("hostname", nargs="*", help=("Hostname of the controller."))
+
+ @asynchronous
+ async def execute(self, origin, options, target):
+ hostnames = None
+ if options.hostname:
+ hostnames = options.hostname
+
+ controller_sets = await asyncio.gather(
+ origin.RackControllers.read(hostnames=hostnames),
+ origin.RegionControllers.read(hostnames=hostnames),
+ )
+ controllers = {
+ controller.system_id: controller
+ for controller in chain.from_iterable(controller_sets)
+ }
+ table = tables.ControllersTable()
+ return table.render(target, controllers.values())
+
+
+class cmd_controller(OriginPagedTableCommand):
+ """Details of a controller."""
+
+ def __init__(self, parser):
+ super(cmd_controller, self).__init__(parser)
+ parser.add_argument("hostname", nargs=1, help=("Hostname of the controller."))
+
+ def execute(self, origin, options, target):
+ nodes = origin.Nodes.read(hostnames=options.hostname)
+ if len(nodes) == 0:
+ raise CommandError("Unable to find controller %s." % options.hostname[0])
+ node = nodes[0]
+ if node.node_type == NodeType.RACK_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_rack_controller()
+ elif node.node_type == NodeType.REGION_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_region_controller()
+ elif node.node_type == NodeType.REGION_AND_RACK_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_rack_controller()
+ else:
+ raise CommandError("Unable to find controller %s." % options.hostname[0])
+ return table.render(target, node)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_controllers.register(parser)
+ cmd_controller.register(parser)
diff --git a/maas/client/flesh/devices.py b/maas/client/flesh/devices.py
new file mode 100644
index 00000000..92d80e5f
--- /dev/null
+++ b/maas/client/flesh/devices.py
@@ -0,0 +1,55 @@
+"""Commands for devices."""
+
+__all__ = ["register"]
+
+from . import CommandError, OriginPagedTableCommand, tables
+
+
+class cmd_devices(OriginPagedTableCommand):
+ """List devices."""
+
+ def __init__(self, parser):
+ super(cmd_devices, self).__init__(parser)
+ parser.add_argument("hostname", nargs="*", help=("Hostname of the device."))
+ parser.add_argument(
+ "--owned", action="store_true", help=("Show only machines owned by you.")
+ )
+
+ def execute(self, origin, options, target):
+ hostnames = None
+ if options.hostname:
+ hostnames = options.hostname
+ devices = origin.Devices.read(hostnames=hostnames)
+ if options.owned:
+ me = origin.Users.whoami()
+ devices = origin.Devices(
+ [
+ device
+ for device in devices
+ if device.owner is not None and device.owner.username == me.username
+ ]
+ )
+ table = tables.DevicesTable()
+ return table.render(target, devices)
+
+
+class cmd_device(OriginPagedTableCommand):
+ """Details of a device."""
+
+ def __init__(self, parser):
+ super(cmd_device, self).__init__(parser)
+ parser.add_argument("hostname", nargs=1, help=("Hostname of the device."))
+
+ def execute(self, origin, options, target):
+ devices = origin.Devices.read(hostnames=options.hostname)
+ if len(devices) == 0:
+ raise CommandError("Unable to find device %s." % options.hostname[0])
+ device = devices[0]
+ table = tables.DeviceDetail()
+ return table.render(target, device)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_devices.register(parser)
+ cmd_device.register(parser)
diff --git a/maas/client/flesh/fabrics.py b/maas/client/flesh/fabrics.py
new file mode 100644
index 00000000..301751ee
--- /dev/null
+++ b/maas/client/flesh/fabrics.py
@@ -0,0 +1,65 @@
+"""Commands for fabrics."""
+
+__all__ = ["register"]
+
+from http import HTTPStatus
+
+from . import CommandError, OriginPagedTableCommand, tables
+from ..bones import CallError
+from ..utils.maas_async import asynchronous
+
+
+class cmd_fabrics(OriginPagedTableCommand):
+ """List fabrics."""
+
+ def __init__(self, parser):
+ super(cmd_fabrics, self).__init__(parser)
+ parser.add_argument(
+ "--minimal", action="store_true", help=("Output only the fabric names.")
+ )
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await fabrics, await subnets
+
+ def execute(self, origin, options, target):
+ visible_columns = None
+ if options.minimal:
+ visible_columns = ("name",)
+ fabrics, subnets = self.load_object_sets(origin)
+ table = tables.FabricsTable(visible_columns=visible_columns, subnets=subnets)
+ return table.render(target, fabrics)
+
+
+class cmd_fabric(OriginPagedTableCommand):
+ """Details of a fabric."""
+
+ def __init__(self, parser):
+ super(cmd_fabric, self).__init__(parser)
+ parser.add_argument("name", nargs=1, help=("Name of the fabric."))
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await fabrics, await subnets
+
+ def execute(self, origin, options, target):
+ try:
+ fabric = origin.Fabric.read(options.name[0])
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ raise CommandError("Unable to find fabric %s." % options.name[0])
+ else:
+ raise
+ fabrics, subnets = self.load_object_sets(origin)
+ table = tables.FabricDetail(fabrics=fabrics, subnets=subnets)
+ return table.render(target, fabric)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_fabrics.register(parser)
+ cmd_fabric.register(parser)
diff --git a/maas/client/flesh/files.py b/maas/client/flesh/files.py
index ca7ba71c..e7ae5765 100644
--- a/maas/client/flesh/files.py
+++ b/maas/client/flesh/files.py
@@ -1,23 +1,18 @@
"""Commands for files."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
-from . import (
- OriginTableCommand,
- tables,
-)
+from . import OriginPagedTableCommand, tables
-class cmd_list_files(OriginTableCommand):
+class cmd_files(OriginPagedTableCommand):
"""List files."""
def execute(self, origin, options, target):
table = tables.FilesTable()
- print(table.render(target, origin.Files))
+ return table.render(target, origin.Files.read())
def register(parser):
"""Register profile commands with the given parser."""
- cmd_list_files.register(parser)
+ cmd_files.register(parser)
diff --git a/maas/client/flesh/machines.py b/maas/client/flesh/machines.py
new file mode 100644
index 00000000..c914fff2
--- /dev/null
+++ b/maas/client/flesh/machines.py
@@ -0,0 +1,1127 @@
+"""Commands for machines."""
+
+__all__ = ["register"]
+
+import asyncio
+import base64
+import os
+import re
+import subprocess
+import sys
+import time
+
+from . import (
+ colorized,
+ CommandError,
+ OriginCommand,
+ OriginPagedTableCommand,
+ tables,
+ yes_or_no,
+)
+from .. import utils
+from ..bones import CallError
+from ..enum import NodeStatus
+from ..utils.maas_async import asynchronous
+
+
+def validate_file(parser, arg):
+ """Validates that `arg` is a valid file."""
+ if not os.path.isfile(arg):
+ parser.error("%s is not a file." % arg)
+ return arg
+
+
+def base64_file(filepath):
+ """Read from `filepath` and convert to base64."""
+ with open(filepath, "rb") as stream:
+ return base64.b64encode(stream.read())
+
+
+class cmd_machines(OriginPagedTableCommand):
+ """List machines."""
+
+ def __init__(self, parser):
+ super(cmd_machines, self).__init__(parser)
+ parser.add_argument("hostname", nargs="*", help=("Hostname of the machine."))
+ parser.add_argument(
+ "--owned", action="store_true", help=("Show only machines owned by you.")
+ )
+
+ def execute(self, origin, options, target):
+ hostnames = None
+ if options.hostname:
+ hostnames = options.hostname
+ machines = origin.Machines.read(hostnames=hostnames)
+ if options.owned:
+ me = origin.Users.whoami()
+ machines = origin.Machines(
+ [
+ machine
+ for machine in machines
+ if machine.owner is not None
+ and machine.owner.username == me.username
+ ]
+ )
+ table = tables.MachinesTable()
+ return table.render(target, machines)
+
+
+class cmd_machine(OriginPagedTableCommand):
+ """Details of a machine."""
+
+ def __init__(self, parser):
+ super(cmd_machine, self).__init__(parser)
+ parser.add_argument("hostname", nargs=1, help=("Hostname of the machine."))
+
+ def execute(self, origin, options, target):
+ machines = origin.Machines.read(hostnames=options.hostname)
+ if len(machines) == 0:
+ raise CommandError("Unable to find machine %s." % options.hostname[0])
+ machine = machines[0]
+ table = tables.MachineDetail()
+ return table.render(target, machine)
+
+
+class cmd_allocate(OriginCommand):
+ """Allocate an available machine.
+
+ Parameters can be used to allocate a machine that possesses
+ certain characteristics. All the parameters are optional and when
+ multiple parameters are provided, they are combined using 'AND'
+ semantics.
+
+ Most parameters map to greater than or equal matching (e.g. --cpus 2, will
+ match any available machine with 2 or more cpus). MAAS uses a cost
+ algorithm to pick the machine based on the parameters that has the lowest
+ usage costs to the remaining availablilty of machines.
+
+ Parameter details:
+
+ --disk
+
+ The machine must have a disk present that is at least X GB in size and
+ have the specified tags. The format of the parameter is:
+
+ [:][([,])]
+
+ Size is specified in GB. Tags map to the tags on the disk. When tags are
+ included the disk must be at least X GB in size and have the specified
+ tags. Label is only used in the resulting acquired machine to provide a
+ mapping between the disk parameter and the disk on the machine that
+ matched that parameter.
+
+ --interface
+
+ Machines must have one or more interfaces. The format of the parameter
+ is:
+
+ [:]=[,=[,...]]
+
+ Each key can be one of the following:
+
+ - id: Matches an interface with the specific id
+ - fabric: Matches an interface attached to the specified fabric.
+ - fabric_class: Matches an interface attached to a fabric
+ with the specified class.
+ - ip: Matches an interface with the specified IP address
+ assigned to it.
+ - mode: Matches an interface with the specified mode. (Currently,
+ the only supported mode is "unconfigured".)
+ - name: Matches an interface with the specified name.
+ (For example, "eth0".)
+ - hostname: Matches an interface attached to the node with
+ the specified hostname.
+ - subnet: Matches an interface attached to the specified subnet.
+ - space: Matches an interface attached to the specified space.
+ - subnet_cidr: Matches an interface attached to the specified
+ subnet CIDR. (For example, "192.168.0.0/24".)
+ - type: Matches an interface of the specified type. (Valid
+ types: "physical", "vlan", "bond", "bridge", or "unknown".)
+ - vlan: Matches an interface on the specified VLAN.
+ - vid: Matches an interface on a VLAN with the specified VID.
+ - tag: Matches an interface tagged with the specified tag.
+
+ --subnet
+
+ The machine must be configured to acquire an address
+ in the specified subnet, have a static IP address in the specified
+ subnet, or have been observed to DHCP from the specified subnet
+ during commissioning time (which implies that it *could* have an
+ address on the specified subnet).
+
+ Subnets can be specified by one of the following criteria:
+
+ - : match the subnet by its 'id' field
+ - fabric:: match all subnets in a given fabric.
+ - ip:: Match the subnet containing with
+ the with the longest-prefix match.
+ - name:: Match a subnet with the given name.
+ - space:: Match all subnets in a given space.
+ - vid:: Match a subnet on a VLAN with the specified
+ VID. Valid values range from 0 through 4094 (inclusive). An
+ untagged VLAN can be specified by using the value "0".
+ - vlan:: Match all subnets on the given VLAN.
+
+ Note that (as of this writing), the 'fabric', 'space', 'vid', and
+ 'vlan' specifiers are only useful for the 'not_spaces' version of
+ this constraint, because they will most likely force the query
+ to match ALL the subnets in each fabric, space, or VLAN, and thus
+ not return any nodes. (This is not a particularly useful behavior,
+ so may be changed in the future.)
+
+ If multiple subnets are specified, the machine must be associated
+ with all of them.
+ """
+
+ def __init__(
+ self, parser, with_hostname=True, with_comment=True, with_dry_run=True
+ ):
+ super(cmd_allocate, self).__init__(parser)
+ if with_hostname:
+ parser.add_argument(
+ "hostname", nargs="?", help=("Hostname of the machine.")
+ )
+ parser.add_argument(
+ "--arch",
+ nargs="*",
+ help=(
+ "Architecture(s) of the desired machine (e.g. 'i386/generic', "
+ "'amd64', 'armhf/highbank', etc.)"
+ ),
+ )
+ parser.add_argument(
+ "--cpus", type=int, help=("Minimum number of CPUs for the desired machine.")
+ )
+ parser.add_argument(
+ "--disk", nargs="*", help=("Disk(s) the desired machine must match.")
+ )
+ parser.add_argument(
+ "--fabric",
+ nargs="*",
+ help=("Fabric(s) the desired machine must be connected to."),
+ )
+ parser.add_argument(
+ "--interface",
+ nargs="*",
+ help=("Interface(s) the desired machine must match."),
+ )
+ parser.add_argument(
+ "--memory",
+ type=float,
+ help=(
+ "Minimum amount of memory (expressed in MB) for the desired " "machine."
+ ),
+ )
+ parser.add_argument(
+ "--pod", help=("Pod the desired machine must be located in.")
+ )
+ parser.add_argument(
+ "--pod-type", help=("Pod type the desired machine must be located in.")
+ )
+ parser.add_argument(
+ "--subnet",
+ nargs="*",
+ help=("Subnet(s) the desired machine must be linked to."),
+ )
+ parser.add_argument(
+ "--tag", nargs="*", help=("Tags the desired machine must match.")
+ )
+ parser.add_argument(
+ "--zone", help=("Zone the desired machine must be located in.")
+ )
+ parser.add_argument(
+ "--not-fabric",
+ nargs="*",
+ help=("Fabric(s) the desired machine must NOT be connected to."),
+ )
+ parser.add_argument(
+ "--not-subnet",
+ nargs="*",
+ help=("Subnets(s) the desired machine must NOT be linked to."),
+ )
+ parser.add_argument(
+ "--not-tag", nargs="*", help=("Tags the desired machine must NOT match.")
+ )
+ parser.add_argument(
+ "--not-zone",
+ nargs="*",
+ help=("Zone(s) the desired machine must NOT belong in."),
+ )
+ parser.other.add_argument(
+ "--agent-name", help=("Agent name to attach to the acquire machine.")
+ )
+ if with_comment:
+ parser.other.add_argument(
+ "--comment", help=("Reason for allocating the machine.")
+ )
+ parser.other.add_argument(
+ "--bridge-all",
+ action="store_true",
+ default=None,
+ help=(
+ "Automatically create a bridge on all interfaces on the "
+ "allocated machine."
+ ),
+ )
+ parser.other.add_argument(
+ "--bridge-stp",
+ action="store_true",
+ default=None,
+ help=(
+ "Turn spaning tree protocol on or off for the bridges created "
+ "with --bridge-all."
+ ),
+ )
+ parser.other.add_argument(
+ "--bridge-fd",
+ type=int,
+ help=(
+ "Set the forward delay in seconds on the bridges created with "
+ "--bridge-all."
+ ),
+ )
+ if with_dry_run:
+ parser.other.add_argument(
+ "--dry-run",
+ action="store_true",
+ default=None,
+ help=(
+ "Don't actually acquire the machine just return the "
+ "machine that would have been acquired."
+ ),
+ )
+
+ @asynchronous
+ async def allocate(self, origin, options):
+ if options.hostname:
+ me = await origin.Users.whoami()
+ machines = await origin.Machines.read(hostnames=[options.hostname])
+ if len(machines) == 0:
+ raise CommandError("Unable to find machine %s." % options.hostname)
+ machine = machines[0]
+ if (
+ machine.status == NodeStatus.ALLOCATED
+ and machine.owner.username == me.username
+ ):
+ return False, machine
+ elif machine.status != NodeStatus.READY:
+ raise CommandError("Unable to allocate machine %s." % options.hostname)
+ params = utils.remove_None(
+ {
+ "hostname": options.hostname,
+ "architectures": options.arch,
+ "cpus": options.cpus,
+ "memory": options.memory,
+ "fabrics": options.fabric,
+ "interfaces": options.interface,
+ "pod": options.pod,
+ "pod_type": options.pod_type,
+ "subnets": options.subnet,
+ "tags": options.tag,
+ "not_fabrics": options.not_fabric,
+ "not_subnets": options.not_subnet,
+ "not_zones": options.not_zone,
+ "agent_name": options.agent_name,
+ "comment": options.comment,
+ "bridge_all": options.bridge_all,
+ "bridge_stp": options.bridge_stp,
+ "bridge_fd": options.bridge_fd,
+ "dry_run": getattr(options, "dry_run", False),
+ }
+ )
+ machine = await origin.Machines.allocate(**params)
+ if options.hostname and machine.hostname != options.hostname:
+ await machine.release()
+ raise CommandError(
+ "MAAS failed to allocate machine %s; "
+ "instead it allocated %s." % (options.hostname, machine.hostname)
+ )
+ return True, machine
+
+ def execute(self, origin, options):
+ with utils.Spinner() as context:
+ context.msg = colorized("{automagenta}Allocating{/automagenta}")
+ _, machine = self.allocate(origin, options)
+ print(colorized("{autoblue}Allocated{/autoblue} %s") % machine.hostname)
+
+
+class MachineWorkMixin:
+ """Mixin that helps with performing actions across a set of machinse."""
+
+ @asynchronous
+ async def _async_perform_action(
+ self, context, action, machines, params, progress_title, success_title
+ ):
+ def _update_msg(remaining):
+ """Update the spinner message."""
+ if len(remaining) == 1:
+ msg = remaining[0].hostname
+ elif len(remaining) == 2:
+ msg = "%s and %s" % (remaining[0].hostname, remaining[1].hostname)
+ else:
+ msg = "%s machines" % len(remaining)
+ context.msg = colorized(
+ "{autoblue}%s{/autoblue} %s" % (progress_title, msg)
+ )
+
+ async def _perform(machine, params, remaining):
+ """Updates the messages as actions complete."""
+ try:
+ await getattr(machine, action)(**params)
+ except Exception as exc:
+ remaining.remove(machine)
+ _update_msg(remaining)
+ context.print(colorized("{autored}Error:{/autored} %s") % str(exc))
+ raise
+ else:
+ remaining.remove(machine)
+ _update_msg(remaining)
+ context.print(
+ colorized("{autogreen}%s{/autogreen} %s")
+ % (success_title, machine.hostname)
+ )
+
+ _update_msg(machines)
+ results = await asyncio.gather(
+ *[_perform(machine, params, machines) for machine in machines],
+ return_exceptions=True
+ )
+ failures = [result for result in results if isinstance(result, Exception)]
+ if len(failures) > 0:
+ return 1
+ return 0
+
+ def perform_action(self, action, machines, params, progress_title, success_title):
+ """Perform the action on the set of machines."""
+ if len(machines) == 0:
+ return 0
+ with utils.Spinner() as context:
+ return self._async_perform_action(
+ context, action, list(machines), params, progress_title, success_title
+ )
+
+ def get_machines(self, origin, hostnames):
+ """Return a set of machines based on `hostnames`.
+
+ Any hostname that is not found will result in an error.
+ """
+ hostnames = {hostname: True for hostname in hostnames}
+ machines = origin.Machines.read(hostnames=hostnames)
+ machines = [
+ machine for machine in machines if hostnames.pop(machine.hostname, False)
+ ]
+ if len(hostnames) > 0:
+ raise CommandError(
+ "Unable to find %s %s."
+ % ("machines" if len(hostnames) > 1 else "machine", ",".join(hostnames))
+ )
+ return machines
+
+
+class MachineSSHMixin:
+ """Mixin that provides the ability to SSH."""
+
+ def add_ssh_options(self, parser):
+ """Add the SSH arguments to the `parser`."""
+ parser.add_argument(
+ "--username", metavar="USER", help=("Username for the SSH connection.")
+ )
+ parser.add_argument(
+ "--boot-only",
+ action="store_true",
+ help=("Only use the IP addresses on the machine's boot interface."),
+ )
+
+ def get_ip_addresses(self, machine, *, boot_only=False, discovered=False):
+ """Return all IP address for `machine`.
+
+ IP address from `boot_interface` come first.
+ """
+ boot_ips = [
+ link.ip_address for link in machine.boot_interface.links if link.ip_address
+ ]
+ if boot_only:
+ if boot_ips:
+ return boot_ips
+ elif discovered:
+ return [
+ link.ip_address
+ for link in machine.boot_interface.discovered
+ if link.ip_address
+ ]
+ else:
+ return []
+ else:
+ other_ips = [
+ link.ip_address
+ for interface in machine.interfaces
+ for link in interface.links
+ if (interface.id != machine.boot_interface.id and link.ip_address)
+ ]
+ ips = boot_ips + other_ips
+ if ips:
+ return ips
+ elif discovered:
+ return [
+ link.ip_address
+ for link in machine.boot_interface.discovered
+ if link.ip_address
+ ] + [
+ link.ip_address
+ for interface in machine.interfaces
+ for link in interface.discovered
+ if (interface.id != machine.boot_interface.id and link.ip_address)
+ ]
+ else:
+ return []
+
+ @asynchronous
+ async def _async_get_sshable_ips(self, ip_addresses):
+ """Return list of all IP address that could be pinged."""
+
+ async def _async_ping(ip_address):
+ try:
+ reader, writer = await asyncio.wait_for(
+ asyncio.open_connection(ip_address, 22), timeout=5
+ )
+ except (OSError, TimeoutError):
+ return None
+ try:
+ line = await reader.readline()
+ finally:
+ writer.close()
+ if line.startswith(b"SSH-"):
+ return ip_address
+
+ ssh_ips = await asyncio.gather(
+ *[_async_ping(ip_address) for ip_address in ip_addresses]
+ )
+ return [ip_address for ip_address in ssh_ips if ip_address is not None]
+
+ def _check_ssh(self, *args):
+ """Check if SSH connection can be made to IP with username."""
+ ssh = subprocess.Popen(
+ args,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ ssh.wait()
+ return ssh.returncode == 0
+
+ def _determine_username(self, ip):
+ """SSH in as root and determine the username."""
+ ssh = subprocess.Popen(
+ [
+ "ssh",
+ "-o",
+ "UserKnownHostsFile=/dev/null",
+ "-o",
+ "StrictHostKeyChecking=no",
+ "root@%s" % ip,
+ ],
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ )
+ first_line = ssh.stdout.readline()
+ ssh.kill()
+ ssh.wait()
+ if first_line:
+ match = re.search(
+ r"Please login as the user \"(\w+)\" rather than "
+ r"the user \"root\".",
+ first_line.decode("utf-8"),
+ )
+ if match:
+ return match.groups()[0]
+ else:
+ return None
+
+ def ssh(
+ self,
+ machine,
+ *,
+ username=None,
+ command=None,
+ boot_only=False,
+ discovered=False,
+ wait=300
+ ):
+ """SSH into `machine`."""
+ start_time = time.monotonic()
+ with utils.Spinner() as context:
+ context.msg = colorized(
+ "{autoblue}Determining{/autoblue} best IP for %s" % (machine.hostname)
+ )
+ ip_addresses = self.get_ip_addresses(
+ machine, boot_only=boot_only, discovered=discovered
+ )
+ if len(ip_addresses) > 0:
+ pingable_ips = self._async_get_sshable_ips(ip_addresses)
+ while len(pingable_ips) == 0 and (time.monotonic() - start_time) < wait:
+ time.sleep(5)
+ pingable_ips = self._async_get_sshable_ips(ip_addresses)
+ if len(pingable_ips) == 0:
+ raise CommandError(
+ "No IP addresses on %s can be reached." % (machine.hostname)
+ )
+ else:
+ ip = pingable_ips[0]
+ else:
+ raise CommandError("%s has no IP addresses." % machine.hostname)
+
+ if username is None:
+ context.msg = colorized(
+ "{autoblue}Determining{/autoblue} SSH username on %s"
+ % (machine.hostname)
+ )
+ username = self._determine_username(ip)
+ while username is None and (time.monotonic() - start_time) < wait:
+ username = self._determine_username(ip)
+ if username is None:
+ raise CommandError("Failed to determine the username for SSH.")
+
+ conn_str = "%s@%s" % (username, ip)
+ args = [
+ "ssh",
+ "-o",
+ "UserKnownHostsFile=/dev/null",
+ "-o",
+ "StrictHostKeyChecking=no",
+ conn_str,
+ ]
+
+ context.msg = colorized(
+ "{automagenta}Waiting{/automagenta} for SSH on %s" % (machine.hostname)
+ )
+ check_args = args + ["echo"]
+ connectable = self._check_ssh(*check_args)
+ while not connectable and (time.monotonic() - start_time) < wait:
+ time.sleep(5)
+ connectable = self._check_ssh(*check_args)
+ if not connectable:
+ raise CommandError(
+ "SSH never started on %s using IP %s." % (machine.hostname, ip)
+ )
+
+ if command is not None:
+ args.append(command)
+ ssh = subprocess.Popen(
+ args, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr
+ )
+ ssh.wait()
+ return ssh.returncode
+
+
+class MachineReleaseMixin(MachineWorkMixin):
+ """Mixin that provide releasing machines."""
+
+ def add_release_options(self, parser):
+ parser.add_argument(
+ "--erase", action="store_true", help=("Erase the disk when releasing.")
+ )
+ parser.add_argument(
+ "--secure-erase",
+ action="store_true",
+ help=("Use the drives secure erase feature if available on the disk."),
+ )
+ parser.add_argument(
+ "--quick-erase",
+ action="store_true",
+ help=(
+ "Wipe the just the beginning and end of the disk. "
+ "This is not secure."
+ ),
+ )
+
+ def get_release_params(self, options):
+ return utils.remove_None(
+ {
+ "erase": options.erase,
+ "secure_erase": options.secure_erase,
+ "quick_erase": options.quick_erase,
+ }
+ )
+
+ def release(self, machines, params):
+ wait = params.get("wait", True)
+ return self.perform_action(
+ "release",
+ machines,
+ params,
+ "Releasing",
+ "Released" if wait else "Releasing",
+ )
+
+
+class cmd_deploy(cmd_allocate, MachineSSHMixin, MachineReleaseMixin):
+ """Allocate and deploy machine.
+
+ See `help allocate` for more details on the allocation parameters.
+ """
+
+ def __init__(self, parser):
+ super(cmd_deploy, self).__init__(
+ parser, with_hostname=False, with_comment=False, with_dry_run=False
+ )
+ parser.add_argument(
+ "image",
+ nargs="?",
+ help=(
+ "Image to deploy to the machine (e.g. ubuntu/xenial or " "just xenial)."
+ ),
+ )
+ parser.add_argument("hostname", nargs="?", help=("Hostname of the machine."))
+ parser.add_argument(
+ "--hwe-kernel",
+ help=(
+ "Hardware enablement kernel to use with the image. Only used "
+ "when deploying Ubuntu."
+ ),
+ )
+ parser.add_argument(
+ "--user-data",
+ metavar="FILE",
+ type=lambda arg: validate_file(parser, arg),
+ help=("User data that gets run on the machine once it has " "deployed."),
+ )
+ parser.add_argument(
+ "--b64-user-data",
+ metavar="BASE64",
+ help=(
+ "Base64 encoded string of the user data that gets run on the "
+ "machine once it has deployed."
+ ),
+ )
+ parser.add_argument(
+ "--ssh",
+ action="store_true",
+ help=("SSH into the machine once its deployed."),
+ )
+ self.add_ssh_options(parser)
+ parser.add_argument(
+ "--release-on-exit",
+ action="store_true",
+ help=(
+ "Release the machine once the SSH connection is closed. "
+ "Only used with --ssh is provided."
+ ),
+ )
+ self.add_release_options(parser)
+ parser.other.add_argument(
+ "--comment", help=("Reason for deploying the machine.")
+ )
+ parser.other.add_argument(
+ "--no-wait",
+ action="store_true",
+ help=("Don't wait for the deploy to complete."),
+ )
+ parser.other.add_argument(
+ "--install-kvm", action="store_true", help=("Install KVM on machine")
+ )
+
+ def _get_deploy_options(self, options):
+ """Return the deployment options based on command line."""
+ user_data = None
+ if options.user_data and options.b64_user_data:
+ raise CommandError("Cannot provide both --user-data and --b64-user-data.")
+ if options.b64_user_data:
+ user_data = options.b64_user_data
+ if options.user_data:
+ user_data = base64_file(options.user_data).decode("ascii")
+ return utils.remove_None(
+ {
+ "distro_series": options.image,
+ "hwe_kernel": options.hwe_kernel,
+ "user_data": user_data,
+ "comment": options.comment,
+ "install_kvm": options.install_kvm,
+ "wait": False,
+ }
+ )
+
+ def _handle_abort(self, machine, allocated):
+ """Handle the user aborting mid deployment."""
+ abort = yes_or_no("Abort deployment?")
+ if abort:
+ with utils.Spinner() as context:
+ if allocated:
+ context.msg = colorized("{autoblue}Releasing{/autoblue} %s") % (
+ machine.hostname
+ )
+ machine.release()
+ context.print(
+ colorized("{autoblue}Released{/autoblue} %s")
+ % (machine.hostname)
+ )
+ else:
+ context.msg = colorized("{autoblue}Aborting{/autoblue} %s") % (
+ machine.hostname
+ )
+ machine.abort()
+ context.print(
+ colorized("{autoblue}Aborted{/autoblue} %s")
+ % (machine.hostname)
+ )
+
+ def execute(self, origin, options):
+ deploy_options = self._get_deploy_options(options)
+ allocated, machine = False, None
+ try:
+ with utils.Spinner() as context:
+ if options.hostname:
+ context.msg = colorized("{autoblue}Allocating{/autoblue} %s") % (
+ options.hostname
+ )
+ else:
+ context.msg = colorized("{autoblue}Searching{/autoblue}")
+ allocated, machine = self.allocate(origin, options)
+ context.msg = (
+ colorized("{autoblue}Deploying{/autoblue} %s") % machine.hostname
+ )
+ try:
+ machine = machine.deploy(**deploy_options)
+ except CallError:
+ if allocated:
+ machine.release()
+ raise
+ if not options.no_wait:
+ context.msg = colorized(
+ "{autoblue}Deploying{/autoblue} %s on %s"
+ ) % (machine.distro_series, machine.hostname)
+ while machine.status == NodeStatus.DEPLOYING:
+ time.sleep(15)
+ machine.refresh()
+ context.msg = colorized(
+ "{autoblue}Deploying{/autoblue} %s on %s: %s"
+ ) % (
+ machine.distro_series,
+ machine.hostname,
+ machine.status_message,
+ )
+ except KeyboardInterrupt:
+ if sys.stdout.isatty() and machine is not None:
+ self._handle_abort(machine, allocated)
+ raise
+
+ if machine.status == NodeStatus.FAILED_DEPLOYMENT:
+ raise CommandError(
+ "Deployment of %s on %s failed."
+ % (machine.distro_series, machine.hostname)
+ )
+ elif machine.status == NodeStatus.DEPLOYED:
+ print(
+ colorized("{autoblue}Deployed{/autoblue} %s on %s")
+ % (machine.distro_series, machine.hostname)
+ )
+ elif machine.status == NodeStatus.DEPLOYING:
+ print(
+ colorized("{autoblue}Deploying{/autoblue} %s on %s")
+ % (machine.distro_series, machine.hostname)
+ )
+ else:
+ raise CommandError(
+ "Machine %s transitioned to an unexpected state of %s."
+ % (machine.hostname, machine.status_name)
+ )
+
+ if options.ssh:
+ machine.refresh()
+ code = self.ssh(
+ machine, username=options.username, boot_only=options.boot_only
+ )
+ if code == 0 and options.release_on_exit:
+ release_params = self.get_release_params(options)
+ release_params["wait"] = True
+ self.release([machine], release_params)
+
+
+class cmd_commission(OriginCommand, MachineSSHMixin, MachineWorkMixin):
+ """Commission machine."""
+
+ def __init__(self, parser):
+ super(cmd_commission, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="*", help=("Hostname of the machine to commission.")
+ )
+ parser.add_argument(
+ "--all",
+ action="store_true",
+ help=("Commission all machines that can be commissioned."),
+ )
+ parser.add_argument(
+ "--new", action="store_true", help=("Commission all new machines.")
+ )
+ parser.add_argument(
+ "--skip-networking",
+ action="store_true",
+ help=(
+ "Skip machine network discovery, keeping the current interface "
+ "configuration for the machine."
+ ),
+ )
+ parser.add_argument(
+ "--skip-storage",
+ action="store_true",
+ help=(
+ "Skip machine storage discovery, keeping the current storage "
+ "configuration for the machine."
+ ),
+ )
+ parser.add_argument(
+ "--scripts",
+ nargs="*",
+ metavar="SCRIPT",
+ help=("Run only the selected commissioning scripts."),
+ )
+ parser.add_argument(
+ "--ssh",
+ action="store_true",
+ help=("SSH into the machine during commissioning."),
+ )
+ self.add_ssh_options(parser)
+ parser.other.add_argument(
+ "--no-wait",
+ action="store_true",
+ help=("Don't wait for the commisisoning to complete."),
+ )
+
+ def execute(self, origin, options):
+ if options.hostname and options.all:
+ raise CommandError("Cannot pass both hostname and --all.")
+ if options.hostname and options.new:
+ raise CommandError("Cannot pass both hostname and --new.")
+ if not options.hostname and not options.all and not options.new:
+ raise CommandError("Missing parameter hostname, --all, or --new.")
+ if options.ssh and (len(options.hostname) > 1 or options.all or options.new):
+ raise CommandError("--ssh can only be used when commissioning one machine.")
+ if options.all:
+ machines = origin.Machines.read()
+ machines = [
+ machine
+ for machine in machines
+ if machine.status
+ in [NodeStatus.NEW, NodeStatus.READY, NodeStatus.FAILED_COMMISSIONING]
+ ]
+ elif options.new:
+ machines = origin.Machines.read()
+ machines = [
+ machine for machine in machines if machine.status == NodeStatus.NEW
+ ]
+ else:
+ machines = self.get_machines(origin, options.hostname)
+ params = utils.remove_None(
+ {
+ "enable_ssh": options.ssh,
+ "skip_networking": options.skip_networking,
+ "skip_storage": options.skip_storage,
+ "commissioning_scripts": options.scripts,
+ "wait": False if options.no_wait else True,
+ }
+ )
+ try:
+ rc = self.perform_action(
+ "commission",
+ machines,
+ params,
+ "Commissioning",
+ "Commissioning" if options.no_wait else "Commissioned",
+ )
+ except KeyboardInterrupt:
+ if sys.stdout.isatty():
+ abort = yes_or_no("Abort commissioning?")
+ if abort:
+ return self.perform_action(
+ "abort", machines, {}, "Aborting", "Aborted"
+ )
+ else:
+ return 1
+ if rc == 0 and len(machines) > 0 and options.ssh:
+ machine = machines[0]
+ machine.refresh()
+ rc = self.ssh(
+ machine,
+ username=options.username,
+ boot_only=options.boot_only,
+ discovered=True,
+ )
+ if rc == 0:
+ return self.perform_action(
+ "power_off", [machine], {}, "Powering off", "Powered off"
+ )
+ return rc
+
+
+class cmd_release(OriginCommand, MachineReleaseMixin):
+ """Release machine."""
+
+ def __init__(self, parser):
+ super(cmd_release, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="*", help=("Hostname of the machine to release.")
+ )
+ parser.add_argument(
+ "--all", action="store_true", help=("Release all machines owned by you.")
+ )
+ parser.add_argument("--comment", help=("Reason for releasing the machine."))
+ self.add_release_options(parser)
+ parser.other.add_argument(
+ "--no-wait",
+ action="store_true",
+ help=("Don't wait for the release to complete."),
+ )
+
+ def execute(self, origin, options):
+ if options.hostname and options.all:
+ raise CommandError("Cannot pass both hostname and --all.")
+ if not options.hostname and not options.all:
+ raise CommandError("Missing parameter hostname or --all.")
+ params = self.get_release_params(options)
+ params["wait"] = False if options.no_wait else True
+ if options.all:
+ me = origin.Users.whoami()
+ machines = origin.Machines.read()
+ machines = [
+ machine
+ for machine in machines
+ if (
+ machine.owner is not None
+ and machine.owner.username == me.username
+ and (
+ machine.status
+ not in [NodeStatus.COMMISSIONING, NodeStatus.TESTING]
+ )
+ )
+ ]
+ else:
+ machines = self.get_machines(origin, options.hostname)
+ return self.release(machines, params)
+
+
+class cmd_abort(OriginCommand, MachineWorkMixin):
+ """Abort machine's current action."""
+
+ def __init__(self, parser):
+ super(cmd_abort, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="+", help=("Hostname of the machine to abort the action.")
+ )
+ parser.add_argument("--comment", help=("Reason for aborting the action."))
+
+ def execute(self, origin, options):
+ params = utils.remove_None({"comment": options.comment})
+ machines = self.get_machines(origin, options.hostname)
+ return self.perform_action("abort", machines, params, "Aborting", "Aborted")
+
+
+class cmd_mark_fixed(OriginCommand, MachineWorkMixin):
+ """Mark machine fixed."""
+
+ def __init__(self, parser):
+ super(cmd_mark_fixed, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="+", help=("Hostname of the machine to mark fixed.")
+ )
+ parser.add_argument("--comment", help=("Reason for marking the machine fixed."))
+
+ def execute(self, origin, options):
+ machines = self.get_machines(origin, options.hostname)
+ return self.perform_action(
+ "mark_fixed", machines, {}, "Marking fixed", "Marked fixed"
+ )
+
+
+class cmd_mark_broken(OriginCommand, MachineWorkMixin):
+ """Mark machine broken."""
+
+ def __init__(self, parser):
+ super(cmd_mark_broken, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="+", help=("Hostname of the machine to mark broken.")
+ )
+ parser.add_argument(
+ "--comment", help=("Reason for marking the machine broken.")
+ )
+
+ def execute(self, origin, options):
+ machines = self.get_machines(origin, options.hostname)
+ return self.perform_action(
+ "mark_broken", machines, {}, "Marking broken", "Marked broken"
+ )
+
+
+class cmd_ssh(OriginCommand, MachineWorkMixin, MachineSSHMixin):
+ """SSH into a machine."""
+
+ def __init__(self, parser):
+ super(cmd_ssh, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs=1, help=("Hostname of the machine to SSH to.")
+ )
+ parser.add_argument(
+ "command",
+ nargs="?",
+ default=None,
+ help=("Hostname of the machine to SSH to."),
+ )
+ self.add_ssh_options(parser)
+
+ def execute(self, origin, options):
+ machine = self.get_machines(origin, options.hostname)[0]
+ return self.ssh(
+ machine,
+ username=options.username,
+ command=options.command,
+ boot_only=options.boot_only,
+ )
+
+
+class cmd_power_on(OriginCommand, MachineWorkMixin):
+ """Power on machine."""
+
+ def __init__(self, parser):
+ super(cmd_power_on, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="+", help=("Hostname of the machine to power on.")
+ )
+ parser.add_argument("--comment", help=("Reason for powering the machine on."))
+
+ def execute(self, origin, options):
+ machines = self.get_machines(origin, options.hostname)
+ return self.perform_action(
+ "power_on", machines, {}, "Powering on", "Powered on"
+ )
+
+
+class cmd_power_off(OriginCommand, MachineWorkMixin):
+ """Power off machine."""
+
+ def __init__(self, parser):
+ super(cmd_power_off, self).__init__(parser)
+ parser.add_argument(
+ "hostname", nargs="+", help=("Hostname of the machine to power off.")
+ )
+ parser.add_argument("--comment", help=("Reason for powering the machine off."))
+
+ def execute(self, origin, options):
+ machines = self.get_machines(origin, options.hostname)
+ return self.perform_action(
+ "power_off", machines, {}, "Powering off", "Powered off"
+ )
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_machines.register(parser)
+ cmd_machine.register(parser)
+ cmd_allocate.register(parser)
+ cmd_deploy.register(parser)
+ cmd_commission.register(parser)
+ cmd_release.register(parser)
+ cmd_abort.register(parser)
+ cmd_mark_fixed.register(parser)
+ cmd_mark_broken.register(parser)
+ cmd_power_off.register(parser)
+ cmd_power_on.register(parser)
+ cmd_ssh.register(parser)
diff --git a/maas/client/flesh/nodes.py b/maas/client/flesh/nodes.py
index 25bfd21f..90c7ac16 100644
--- a/maas/client/flesh/nodes.py
+++ b/maas/client/flesh/nodes.py
@@ -1,151 +1,57 @@
"""Commands for nodes."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
-from itertools import chain
-from time import sleep
+from . import CommandError, OriginPagedTableCommand, tables
+from ..enum import NodeType
-from . import (
- colorized,
- CommandError,
- OriginTableCommand,
- tables,
-)
-from .. import utils
-
-class cmd_allocate_machine(OriginTableCommand):
- """Allocate a machine."""
-
- def __init__(self, parser):
- super(cmd_allocate_machine, self).__init__(parser)
- parser.add_argument("--hostname")
- parser.add_argument("--architecture")
- parser.add_argument("--cpus", type=int)
- parser.add_argument("--memory", type=float)
- parser.add_argument("--tags", default="")
-
- def allocate(self, origin, options):
- return origin.Machines.allocate(
- hostname=options.hostname, architecture=options.architecture,
- cpus=options.cpus, memory=options.memory,
- tags=options.tags.split())
-
- def execute(self, origin, options, target):
- machine = self.allocate(origin, options)
- table = tables.NodesTable()
- print(table.render(target, [machine]))
-
-
-class cmd_launch_machine(cmd_allocate_machine):
- """Allocate and deploy a machine."""
-
- def __init__(self, parser):
- super(cmd_launch_machine, self).__init__(parser)
- parser.add_argument(
- "--wait", type=int, default=0, help=(
- "Number of seconds to wait for deploy to complete."))
-
- def execute(self, origin, options, target):
- machine = self.allocate(origin, options)
- table = tables.NodesTable()
-
- print(colorized("{automagenta}DEPLOYING:{/automagenta}"))
- print(table.render(target, [machine]))
-
- with utils.Spinner():
- machine = machine.start()
- for elapsed, remaining, wait in utils.retries(options.wait, 1.0):
- if machine.status_name == "Deploying":
- sleep(wait)
- machine = origin.Machine.read(system_id=machine.system_id)
- else:
- break
-
- if machine.status_name == "Deployed":
- print(colorized("{autogreen}DEPLOYED:{/autogreen}"))
- print(table.render(target, [machine]))
- else:
- print(colorized("{autored}FAILED TO DEPLOY:{/autored}"))
- print(table.render(target, [machine]))
- raise CommandError("Machine was not deployed.")
-
-
-class cmd_release_machine(OriginTableCommand):
- """Release a machine."""
+class cmd_nodes(OriginPagedTableCommand):
+ """List nodes."""
def __init__(self, parser):
- super(cmd_release_machine, self).__init__(parser)
- parser.add_argument("--system-id", required=True)
- parser.add_argument(
- "--wait", type=int, default=0, help=(
- "Number of seconds to wait for release to complete."))
+ super(cmd_nodes, self).__init__(parser)
+ parser.add_argument("hostname", nargs="*", help=("Hostname of the node."))
def execute(self, origin, options, target):
- machine = origin.Node.read(system_id=options.system_id)
- machine = machine.release()
-
- with utils.Spinner():
- for elapsed, remaining, wait in utils.retries(options.wait, 1.0):
- if machine.status_name == "Releasing":
- sleep(wait)
- machine = origin.Node.read(system_id=machine.system_id)
- else:
- break
-
+ hostnames = None
+ if options.hostname:
+ hostnames = options.hostname
table = tables.NodesTable()
- print(table.render(target, [machine]))
-
- if machine.status_name != "Ready":
- raise CommandError("Machine was not released.")
+ return table.render(target, origin.Nodes.read(hostnames=hostnames))
-class cmd_list_nodes(OriginTableCommand):
- """List machine, devices, rack & region controllers."""
+class cmd_node(OriginPagedTableCommand):
+ """Details of a node."""
def __init__(self, parser):
- super(cmd_list_nodes, self).__init__(parser)
- parser.add_argument(
- "--all", action="store_true", default=False,
- help="Show all (machines, devices, rack & region controllers).")
- parser.add_argument(
- "--devices", action="store_true", default=False,
- help="Show devices.")
- parser.add_argument(
- "--machines", action="store_true", default=False,
- help="Show machines.")
- parser.add_argument(
- "--rack-controllers", action="store_true", default=False,
- help="Show rack-controllers.")
- # parser.add_argument(
- # "--region-controllers", action="store_true", default=False,
- # help="Show region controllers.")
+ super(cmd_node, self).__init__(parser)
+ parser.add_argument("hostname", nargs=1, help=("Hostname of the node."))
def execute(self, origin, options, target):
- nodes = []
-
- if options.all or options.devices:
- nodes.append(origin.Devices)
- if options.all or options.machines:
- nodes.append(origin.Machines)
- if options.all or options.rack_controllers:
- nodes.append(origin.RackControllers)
- # if options.all or options.regions:
- # nodes.append(origin.Regions)
-
+ nodes = origin.Nodes.read(hostnames=options.hostname)
if len(nodes) == 0:
- nodes.append(origin.Machines)
-
- nodes = chain.from_iterable(nodes)
- table = tables.NodesTable()
- print(table.render(target, nodes))
+ raise CommandError("Unable to find node %s." % options.hostname[0])
+ node = nodes[0]
+ if node.node_type == NodeType.MACHINE:
+ table = tables.MachineDetail(with_type=True)
+ node = node.as_machine()
+ elif node.node_type == NodeType.DEVICE:
+ table = tables.DeviceDetail(with_type=True)
+ node = node.as_device()
+ elif node.node_type == NodeType.RACK_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_rack_controller()
+ elif node.node_type == NodeType.REGION_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_region_controller()
+ elif node.node_type == NodeType.REGION_AND_RACK_CONTROLLER:
+ table = tables.ControllerDetail()
+ node = node.as_rack_controller()
+ return table.render(target, node)
def register(parser):
"""Register commands with the given parser."""
- cmd_list_nodes.register(parser, "list")
- cmd_allocate_machine.register(parser, "allocate")
- cmd_launch_machine.register(parser, "launch")
- cmd_release_machine.register(parser, "release")
+ cmd_nodes.register(parser)
+ cmd_node.register(parser)
diff --git a/maas/client/flesh/profiles.py b/maas/client/flesh/profiles.py
index 99b1ec9f..195479b2 100644
--- a/maas/client/flesh/profiles.py
+++ b/maas/client/flesh/profiles.py
@@ -1,66 +1,27 @@
"""Commands for working with local profiles."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
import sys
from . import (
colorized,
Command,
+ print_with_pager,
PROFILE_DEFAULT,
PROFILE_NAMES,
+ read_input,
TableCommand,
tables,
)
-from .. import (
- bones,
- utils,
-)
-from ..utils import (
- auth,
- login,
- profiles,
-)
-
-
-class cmd_login_base(Command):
-
- def __init__(self, parser):
- super(cmd_login_base, self).__init__(parser)
- parser.add_argument(
- "profile_name", metavar="profile-name", help=(
- "The name with which you will later refer to this remote "
- "server and credentials within this tool."
- ))
- parser.add_argument(
- "url", type=utils.api_url, help=(
- "The URL of the remote API, e.g. http://example.com/MAAS/ "
- "or http://example.com/MAAS/api/2.0/ if you wish to specify "
- "the API version."))
- parser.add_argument(
- '-k', '--insecure', action='store_true', help=(
- "Disable SSL certificate check"), default=False)
-
- @staticmethod
- def print_whats_next(profile):
- """Explain what to do next."""
- what_next = [
- "{{autogreen}}Congratulations!{{/autogreen}} You are logged in "
- "to the MAAS server at {{autoblue}}{profile.url}{{/autoblue}} "
- "with the profile name {{autoblue}}{profile.name}{{/autoblue}}.",
- "For help with the available commands, try:",
- " maas --help",
- ]
- for message in what_next:
- message = message.format(profile=profile)
- print(colorized(message))
- print()
+from .. import bones, utils
+from ..bones import helpers
+from ..utils import auth, profiles
+from ..utils.maas_async import asynchronous
-class cmd_login(cmd_login_base):
- """Log-in to a remote MAAS with username and password.
+class cmd_login(Command):
+ """Log-in to a MAAS with either username and password or apikey.
The username and password will NOT be saved; a new API key will be
obtained from MAAS and associated with the new profile. This key can be
@@ -70,102 +31,165 @@ class cmd_login(cmd_login_base):
def __init__(self, parser):
super(cmd_login, self).__init__(parser)
parser.add_argument(
- "username", nargs="?", default=None, help=(
- "The username used to login to MAAS. Omit this and the "
- "password for anonymous API access."))
+ "-p",
+ "--profile-name",
+ default=None,
+ help=(
+ "The name to give the profile. Default is the username used "
+ "to login."
+ ),
+ )
+ parser.add_argument(
+ "--anonymous",
+ default=False,
+ action="store_true",
+ help=(
+ "Create an anonymous profile, no credentials are associated " "to it."
+ ),
+ )
parser.add_argument(
- "password", nargs="?", default=None, help=(
- "The password used to login to MAAS. Omit both the username "
- "and the password for anonymous API access, or pass a single "
- "hyphen to allow the password to be provided via standard-"
- "input. If a username is provided but no password, the "
- "password will be prompted for, interactively."
+ "--apikey",
+ default=None,
+ help=(
+ "The API key acquired from MAAS. This requires the profile "
+ "name to be provided as well."
),
)
-
- def __call__(self, options):
- # Special-case when password is "-", meaning read from stdin.
- if options.password == "-":
- options.password = sys.stdin.readline().strip()
-
- while True:
- try:
- profile = login.login(
- options.url, username=options.username,
- password=options.password, insecure=options.insecure)
- except login.UsernameWithoutPassword:
- # Try to obtain the password interactively.
- options.password = auth.try_getpass("Password: ")
- if options.password is None:
- raise
- else:
- break
-
- # Give it the name the user wanted.
- profile = profile.replace(name=options.profile_name)
-
- # Save a new profile.
- with profiles.ProfileStore.open() as config:
- config.save(profile)
- config.default = profile
-
- self.print_whats_next(profile)
-
-
-class cmd_add(cmd_login_base):
- """Add a profile for a remote MAAS using an *API key*.
-
- The `login` command will typically be more convenient.
- """
-
- def __init__(self, parser):
- super(cmd_add, self).__init__(parser)
parser.add_argument(
- "credentials", nargs="?", default=None, help=(
- "The credentials, also known as the API key, for the remote "
- "MAAS server. These can be found in the user preferences page "
- "in the Web UI; they take the form of a long random-looking "
- "string composed of three parts, separated by colons. Specify "
- "an empty string for anonymous API access, or pass a single "
- "hyphen to allow the credentials to be provided via standard-"
- "input. If no credentials are provided, they will be prompted "
+ "-k",
+ "--insecure",
+ action="store_true",
+ help=("Disable SSL certificate check"),
+ default=False,
+ )
+ parser.add_argument(
+ "url",
+ nargs="?",
+ type=utils.api_url,
+ help=(
+ "The URL of the API, e.g. http://example.com/MAAS/ "
+ "or http://example.com/MAAS/api/2.0/ if you wish to specify "
+ "the API version. If no URL is provided then it will be "
+ "prompted for, interactively."
+ ),
+ )
+ parser.add_argument(
+ "username",
+ nargs="?",
+ default=None,
+ help=(
+ "The username used to login to MAAS. If no username is "
+ "provided and API key is not being used it will be prompted "
+ "for, interactively."
+ ),
+ )
+ parser.add_argument(
+ "password",
+ nargs="?",
+ default=None,
+ help=(
+ "The password used to login to MAAS. If no password is "
+ "proviced and API key is not being used it will be promoed "
"for, interactively."
),
)
- def __call__(self, options):
- # Try and obtain credentials interactively if they're not given, or
- # read them from stdin if they're specified as "-".
- credentials = auth.obtain_credentials(options.credentials)
- # Establish a session with the remote API.
- session = bones.SessionAPI.fromURL(
- options.url, credentials=credentials, insecure=options.insecure)
- # Make a new profile and save it as the default.
- profile = profiles.Profile(
- options.profile_name, options.url, credentials=credentials,
- description=session.description)
+ @asynchronous
+ async def __call__(self, options):
+ has_auth_info = any((options.apikey, options.username, options.password))
+ if options.anonymous and has_auth_info:
+ raise ValueError(
+ "Can't specify username, password or--apikey with --anonymous"
+ )
+
+ if options.apikey and not options.profile_name:
+ raise ValueError("-p,--profile-name must be provided with --apikey")
+
+ if not options.url:
+ url = read_input("URL", validator=utils.api_url)
+ else:
+ url = options.url
+
+ if not options.apikey:
+ if options.anonymous:
+ password = None
+ elif options.username and not options.password:
+ password = read_input("Password", password=True)
+ else:
+ password = options.password
+ if password == "-":
+ password = sys.stdin.readline().strip()
+ try:
+ profile = await helpers.login(
+ url,
+ anonymous=options.anonymous,
+ username=options.username,
+ password=password,
+ insecure=options.insecure,
+ )
+ except helpers.MacaroonLoginNotSupported:
+ # the server doesn't have external authentication enabled,
+ # propmt for username/password
+ username = read_input("Username")
+ password = read_input("Password", password=True)
+ profile = await helpers.login(
+ url, username=username, password=password, insecure=options.insecure
+ )
+ else:
+ credentials = auth.obtain_credentials(options.apikey)
+ session = await bones.SessionAPI.fromURL(
+ url, credentials=credentials, insecure=options.insecure
+ )
+ profile = profiles.Profile(
+ options.profile_name,
+ url,
+ credentials=credentials,
+ description=session.description,
+ )
+
+ if options.profile_name:
+ profile = profile.replace(name=options.profile_name)
+
+ # Save a new profile.
with profiles.ProfileStore.open() as config:
config.save(profile)
config.default = profile
self.print_whats_next(profile)
+ @staticmethod
+ def print_whats_next(profile):
+ """Explain what to do next."""
+ what_next = [
+ "{{autogreen}}Congratulations!{{/autogreen}} You are logged in "
+ "to the MAAS server at {{autoblue}}{profile.url}{{/autoblue}} "
+ "with the profile name {{autoblue}}{profile.name}{{/autoblue}}.",
+ "For help with the available commands, try:",
+ " maas help",
+ ]
+ for message in what_next:
+ message = message.format(profile=profile)
+ print(colorized(message))
+ print()
-class cmd_remove(Command):
- """Remove a profile, purging any stored credentials.
+
+class cmd_logout(Command):
+ """Logout of a MAAS profile, purging any stored credentials.
This will remove the given profile from your command-line client. You can
re-create it later using `add` or `login`.
"""
def __init__(self, parser):
- super(cmd_remove, self).__init__(parser)
+ super(cmd_logout, self).__init__(parser)
parser.add_argument(
- "profile_name", metavar="profile-name",
- nargs="?", choices=PROFILE_NAMES, help=(
- "The name with which a remote server and its "
- "credentials are referred to within this tool." +
- ("" if PROFILE_DEFAULT is None else " [default: %(default)s]")
+ "profile_name",
+ metavar="profile-name",
+ nargs="?",
+ choices=PROFILE_NAMES,
+ help=(
+ "The profile name you want to logout of."
+ + ("" if PROFILE_DEFAULT is None else " [default: %(default)s]")
),
)
if PROFILE_DEFAULT is not None:
@@ -177,16 +201,19 @@ def __call__(self, options):
class cmd_switch(Command):
- """Switch the default profile."""
+ """Switch the active profile.
+
+ This will switch the currently active profile to the given profile. The
+ previous profile will remain, just use `switch` again to go back.
+ """
def __init__(self, parser):
super(cmd_switch, self).__init__(parser)
parser.add_argument(
- "profile_name", metavar="profile-name", choices=PROFILE_NAMES,
- help=(
- "The name with which a remote server and its credentials "
- "are referred to within this tool."
- ),
+ "profile_name",
+ metavar="profile-name",
+ choices=PROFILE_NAMES,
+ help=("The profile name you want to switch to."),
)
def __call__(self, options):
@@ -195,55 +222,47 @@ def __call__(self, options):
config.default = profile
-class cmd_list(TableCommand):
- """List remote APIs that have been logged-in to."""
-
- def __call__(self, options):
- table = tables.ProfilesTable()
- with profiles.ProfileStore.open() as config:
- print(table.render(options.format, config))
-
-
-class cmd_refresh(Command):
- """Refresh the API descriptions of all profiles.
+class cmd_profiles(TableCommand):
+ """List profiles (aka. logged in MAAS's)."""
- This retrieves the latest version of the help information for each
- profile. Use it to update your command-line client's information after
- an upgrade to the MAAS server.
- """
+ def __init__(self, parser):
+ super(cmd_profiles, self).__init__(parser)
+ parser.add_argument(
+ "--refresh",
+ action="store_true",
+ default=False,
+ help=(
+ "Retrieves the latest version of the help information for "
+ "all profiles. Use it to update your command-line client's "
+ "information after an upgrade to the MAAS server."
+ ),
+ )
+ parser.other.add_argument(
+ "--no-pager",
+ action="store_true",
+ help=("Don't use the pager when printing the output of the " "command."),
+ )
def __call__(self, options):
- with profiles.ProfileStore.open() as config:
- for profile_name in config:
- profile = config.load(profile_name)
- session = bones.SessionAPI.fromProfile(profile)
- profile = profile.replace(description=session.description)
- config.save(profile)
+ if options.refresh:
+ with profiles.ProfileStore.open() as config:
+ for profile_name in config:
+ profile = config.load(profile_name)
+ session = bones.SessionAPI.fromProfile(profile)
+ profile = profile.replace(description=session.description)
+ config.save(profile)
+ else:
+ table = tables.ProfilesTable()
+ with profiles.ProfileStore.open() as config:
+ if options.no_pager:
+ print(table.render(options.format, config))
+ else:
+ print_with_pager(table.render(options.format, config))
def register(parser):
"""Register profile commands with the given parser."""
-
- # Register `login`, `logout`, and `switch` as top-level commands.
- cmd_login.register(parser)
- cmd_remove.register(parser, "logout")
- cmd_switch.register(parser)
-
- # Register the complete set of commands with the `profiles` sub-parser.
- parser = parser.subparsers.add_parser(
- "profiles", help="Manage profiles, e.g. adding, removing, logging-in.",
- description=(
- "A profile is a convenient way to refer to a remote MAAS "
- "installation. It encompasses the URL, the credentials, and "
- "the retrieved API description. Each profile has a unique name "
- "which can be provided to commands that work with remote MAAS "
- "installations, or a default profile can be chosen."
- ),
- )
-
- cmd_add.register(parser)
- cmd_remove.register(parser)
cmd_login.register(parser)
- cmd_list.register(parser)
+ cmd_logout.register(parser)
cmd_switch.register(parser)
- cmd_refresh.register(parser)
+ cmd_profiles.register(parser)
diff --git a/maas/client/flesh/shell.py b/maas/client/flesh/shell.py
index cafa1632..b966d4af 100644
--- a/maas/client/flesh/shell.py
+++ b/maas/client/flesh/shell.py
@@ -1,28 +1,18 @@
"""Commands for running interactive and non-interactive shells."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
import code
import sys
import textwrap
+import tokenize
-from . import (
- colorized,
- Command,
- PROFILE_DEFAULT,
- PROFILE_NAMES,
-)
-from .. import (
- bones,
- viscera,
-)
-from ..utils import profiles
+from . import colorized, Command, PROFILE_DEFAULT, PROFILE_NAMES
+from .. import bones, facade, viscera
class cmd_shell(Command):
- """Start a shell with some convenient local variables.
+ """Start a python shell to interact with python-libmaas.
If IPython is available it will be used, otherwise the familiar Python
REPL will be started. If a script is piped in, it is read in its entirety
@@ -30,8 +20,7 @@ class cmd_shell(Command):
"""
profile_name_choices = PROFILE_NAMES
- profile_name_default = (
- None if PROFILE_DEFAULT is None else PROFILE_DEFAULT.name)
+ profile_name_default = None if PROFILE_DEFAULT is None else PROFILE_DEFAULT.name
def __init__(self, parser):
super(cmd_shell, self).__init__(parser)
@@ -41,90 +30,138 @@ def __init__(self, parser):
# message instead of something more cryptic. Note that the help
# string differs too.
parser.add_argument(
- "--profile-name", metavar="NAME", required=False,
- default=None, help=(
+ "--profile-name",
+ metavar="NAME",
+ required=False,
+ default=None,
+ help=(
"The name of the remote MAAS instance to use. "
"No profiles are currently defined; use the `profiles` "
"command to create one."
- ))
+ ),
+ )
else:
parser.add_argument(
- "--profile-name", metavar="NAME", required=False,
+ "--profile-name",
+ metavar="NAME",
+ required=False,
choices=self.profile_name_choices,
- default=self.profile_name_default, help=(
- "The name of the remote MAAS instance to use." + (
- "" if self.profile_name_default is None
+ default=self.profile_name_default,
+ help=(
+ "The name of the remote MAAS instance to use."
+ + (
+ ""
+ if self.profile_name_default is None
else " [default: %(default)s]"
)
- ))
+ ),
+ )
+ parser.add_argument(
+ "--viscera",
+ action="store_true",
+ default=False,
+ help=(
+ "Create a pre-canned viscera `Origin` for the selected "
+ "profile. This is available as `origin` in the shell's "
+ "namespace. You probably do not need this unless you're "
+ "developing python-libmaas itself."
+ ),
+ )
+ parser.add_argument(
+ "--bones",
+ action="store_true",
+ default=False,
+ help=(
+ "Create a pre-canned bones `Session` for the selected "
+ "profile. This is available as `session` in the shell's "
+ "namespace. You probably do not need this unless you're "
+ "developing python-libmaas itself."
+ ),
+ )
+ parser.add_argument(
+ "script",
+ metavar="SCRIPT",
+ nargs="?",
+ default=None,
+ help=(
+ "Python script to run in the shell's namespace. An "
+ "interactive shell is started if none is given."
+ ),
+ )
def __call__(self, options):
"""Execute this command."""
# The namespace that code will run in.
- namespace = {
- "Origin": viscera.Origin,
- "Session": bones.SessionAPI,
- "ProfileStore": profiles.ProfileStore,
- }
+ namespace = {}
# Descriptions of the namespace variables.
- descriptions = {
- "Origin": (
- "The entry-point into the `viscera` higher-level API. "
- "Get started with `Origin.login`."
- ),
- "Session": (
- "The entry-point into the `bones` lower-level API. "
- "Get started with `SessionAPI.login`."
- ),
- "ProfileStore": (
- "Use `ProfileStore.open()` as a context-manager to "
- "work with your profile store."
- ),
- }
+ descriptions = {}
# If a profile has been selected, set up a `bones` session and a
# `viscera` origin in the default namespace.
if options.profile_name is not None:
session = bones.SessionAPI.fromProfileName(options.profile_name)
- namespace["session"] = session
- descriptions["session"] = (
- "A pre-canned `bones` session for '%s'."
- % options.profile_name)
+ if options.bones:
+ namespace["session"] = session
+ descriptions["session"] = (
+ "A pre-canned `bones` session for '%s'." % options.profile_name
+ )
origin = viscera.Origin(session)
- namespace["origin"] = origin
- descriptions["origin"] = (
- "A pre-canned `viscera` origin for '%s'."
- % options.profile_name)
-
- if sys.stdin.isatty() and sys.stdout.isatty():
- # We at a fully interactive terminal — i.e. stdin AND stdout are
- # connected to the TTY — so display some introductory text...
- banner = ["{automagenta}Welcome to the MAAS shell.{/automagenta}"]
- if len(descriptions) > 0:
- banner += ["", "Predefined objects:", ""]
- wrap = textwrap.TextWrapper(60, " ", " ").wrap
- sortkey = lambda name: (name.casefold(), name)
- for name in sorted(descriptions, key=sortkey):
- banner.append(" {autoyellow}%s{/autoyellow}:" % name)
- banner.extend(wrap(descriptions[name]))
- banner.append("")
- for line in banner:
- print(colorized(line))
- # ... then start IPython, or the plain familiar Python REPL if
- # IPython is not installed.
- try:
- import IPython
- except ImportError:
- code.InteractiveConsole(namespace).interact(" ")
+ if options.viscera:
+ namespace["origin"] = origin
+ descriptions["origin"] = (
+ "A pre-canned `viscera` origin for '%s'." % options.profile_name
+ )
+ client = facade.Client(origin)
+ namespace["client"] = client
+ descriptions["client"] = (
+ "A pre-canned client for '%s'." % options.profile_name
+ )
+
+ if options.script is None:
+ if sys.stdin.isatty() and sys.stdout.isatty():
+ self._run_interactive(namespace, descriptions)
else:
- IPython.start_ipython(
- argv=[], display_banner=False, user_ns=namespace)
+ self._run_stdin(namespace)
+ else:
+ self._run_script(namespace, options.script)
+
+ @staticmethod
+ def _run_interactive(namespace, descriptions):
+ # We at a fully interactive terminal — i.e. stdin AND stdout are
+ # connected to the TTY — so display some introductory text...
+ banner = ["{automagenta}Welcome to the MAAS shell.{/automagenta}"]
+ if len(descriptions) > 0:
+ banner += ["", "Predefined objects:", ""]
+ wrap = textwrap.TextWrapper(60, " ", " ").wrap
+ sortkey = lambda name: (name.casefold(), name)
+ for name in sorted(descriptions, key=sortkey):
+ banner.append(" {autoyellow}%s{/autoyellow}:" % name)
+ banner.extend(wrap(descriptions[name]))
+ banner.append("")
+ for line in banner:
+ print(colorized(line))
+ # ... then start IPython, or the plain familiar Python REPL if
+ # IPython is not installed.
+ try:
+ import IPython
+ except ImportError:
+ code.InteractiveConsole(namespace).interact(" ")
else:
- # Either stdin or stdout is NOT connected to the TTY, so simply
- # slurp from stdin and exec in the already created namespace.
- source = sys.stdin.read()
- exec(source, namespace, namespace)
+ IPython.start_ipython(argv=[], display_banner=False, user_ns=namespace)
+
+ @staticmethod
+ def _run_script(namespace, filename):
+ namespace = dict(namespace, __file__=filename)
+ with tokenize.open(filename) as fd:
+ code = compile(fd.read(), filename, "exec")
+ exec(code, namespace, namespace)
+
+ @staticmethod
+ def _run_stdin(namespace):
+ namespace = dict(namespace, __file__="")
+ code = compile(sys.stdin.read(), "", "exec")
+ exec(code, namespace, namespace)
def register(parser):
diff --git a/maas/client/flesh/spaces.py b/maas/client/flesh/spaces.py
new file mode 100644
index 00000000..1d37c1c9
--- /dev/null
+++ b/maas/client/flesh/spaces.py
@@ -0,0 +1,68 @@
+"""Commands for spaces."""
+
+__all__ = ["register"]
+
+from http import HTTPStatus
+
+from . import CommandError, OriginPagedTableCommand, tables
+from ..bones import CallError
+from ..utils.maas_async import asynchronous
+
+
+class cmd_spaces(OriginPagedTableCommand):
+ """List spaces."""
+
+ def __init__(self, parser):
+ super(cmd_spaces, self).__init__(parser)
+ parser.add_argument(
+ "--minimal", action="store_true", help=("Output only the space names.")
+ )
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ spaces = origin.Spaces.read()
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await spaces, await fabrics, await subnets
+
+ def execute(self, origin, options, target):
+ visible_columns = None
+ if options.minimal:
+ visible_columns = ("name",)
+ spaces, fabrics, subnets = self.load_object_sets(origin)
+ table = tables.SpacesTable(
+ visible_columns=visible_columns, fabrics=fabrics, subnets=subnets
+ )
+ return table.render(target, spaces)
+
+
+class cmd_space(OriginPagedTableCommand):
+ """Details of a space."""
+
+ def __init__(self, parser):
+ super(cmd_space, self).__init__(parser)
+ parser.add_argument("name", nargs=1, help=("Name of the space."))
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await fabrics, await subnets
+
+ def execute(self, origin, options, target):
+ try:
+ space = origin.Space.read(options.name[0])
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ raise CommandError("Unable to find space %s." % options.name[0])
+ else:
+ raise
+ fabrics, subnets = self.load_object_sets(origin)
+ table = tables.SpaceDetail(fabrics=fabrics, subnets=subnets)
+ return table.render(target, space)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_spaces.register(parser)
+ cmd_space.register(parser)
diff --git a/maas/client/flesh/subnets.py b/maas/client/flesh/subnets.py
new file mode 100644
index 00000000..c8f7a094
--- /dev/null
+++ b/maas/client/flesh/subnets.py
@@ -0,0 +1,58 @@
+"""Commands for subnets."""
+
+__all__ = ["register"]
+
+from http import HTTPStatus
+
+from . import CommandError, OriginPagedTableCommand, tables
+from ..bones import CallError
+from ..utils.maas_async import asynchronous
+
+
+class cmd_subnets(OriginPagedTableCommand):
+ """List subnets."""
+
+ def __init__(self, parser):
+ super(cmd_subnets, self).__init__(parser)
+ parser.add_argument(
+ "--minimal", action="store_true", help=("Output only the subnet names.")
+ )
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ subnets = origin.Subnets.read()
+ fabrics = origin.Fabrics.read()
+ return await subnets, await fabrics
+
+ def execute(self, origin, options, target):
+ visible_columns = None
+ if options.minimal:
+ visible_columns = ("name",)
+ subnets, fabrics = self.load_object_sets(origin)
+ table = tables.SubnetsTable(visible_columns=visible_columns, fabrics=fabrics)
+ return table.render(target, subnets)
+
+
+class cmd_subnet(OriginPagedTableCommand):
+ """Details of a subnet."""
+
+ def __init__(self, parser):
+ super(cmd_subnet, self).__init__(parser)
+ parser.add_argument("name", nargs=1, help=("Name of the subnet."))
+
+ def execute(self, origin, options, target):
+ try:
+ subnet = origin.Subnet.read(options.name[0])
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ raise CommandError("Unable to find subnet %s." % options.name[0])
+ else:
+ raise
+ table = tables.SubnetDetail(fabrics=origin.Fabrics.read())
+ return table.render(target, subnet)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_subnets.register(parser)
+ cmd_subnet.register(parser)
diff --git a/maas/client/flesh/tables.py b/maas/client/flesh/tables.py
index 871f317b..70047590 100644
--- a/maas/client/flesh/tables.py
+++ b/maas/client/flesh/tables.py
@@ -1,64 +1,45 @@
"""Tables for representing information from MAAS."""
-__all__ = [
- "FilesTable",
- "NodesTable",
- "ProfilesTable",
- "TagsTable",
- "UsersTable",
-]
+__all__ = ["FilesTable", "NodesTable", "ProfilesTable", "TagsTable", "UsersTable"]
-from operator import itemgetter
+from operator import attrgetter, itemgetter
from colorclass import Color
-from ..viscera.controllers import RackController
-from ..viscera.devices import Device
-from ..viscera.machines import Machine
-from .tabular import (
- Column,
- RenderTarget,
- Table,
-)
+from ..enum import InterfaceType, NodeType, PowerState, RDNSMode
+from .tabular import Column, RenderTarget, Table, DetailTable, NestedTableColumn
class NodeTypeColumn(Column):
- DEVICE = 1
- MACHINE = 2
- RACK = 3
- REGION = 4
-
- GLYPHS = {
- DEVICE: Color("."),
- MACHINE: Color("{autoblue}m{/autoblue}"),
- RACK: Color("{yellow}c{/yellow}"),
- REGION: Color("{automagenta}C{/automagenta}"),
+ nice_names = {
+ NodeType.MACHINE: "Machine",
+ NodeType.DEVICE: "Device",
+ NodeType.RACK_CONTROLLER: "Rackd",
+ NodeType.REGION_CONTROLLER: "Regiond",
+ NodeType.REGION_AND_RACK_CONTROLLER: "Regiond+rackd",
}
- def render(self, target, num):
- glyph = self.GLYPHS[num]
- if target == RenderTarget.pretty:
- return glyph
+ def render(self, target, node_type):
+ if target in (RenderTarget.pretty, RenderTarget.plain):
+ node_type = self.nice_names[node_type]
else:
- return glyph.value_no_colors
+ node_type = node_type.value
+ return super().render(target, node_type)
class NodeArchitectureColumn(Column):
-
def render(self, target, architecture):
if target in (RenderTarget.pretty, RenderTarget.plain):
- if architecture is None:
- architecture = "-"
- elif len(architecture) == 0:
- architecture = "-"
- elif architecture.isspace():
+ if architecture:
+ if architecture.endswith("/generic"):
+ architecture = architecture[:-8]
+ else:
architecture = "-"
return super().render(target, architecture)
class NodeCPUsColumn(Column):
-
def render(self, target, cpus):
# `cpus` is a count of CPUs.
if target in (RenderTarget.pretty, RenderTarget.plain):
@@ -70,7 +51,6 @@ def render(self, target, cpus):
class NodeMemoryColumn(Column):
-
def render(self, target, memory):
# `memory` is in MB.
if target in (RenderTarget.pretty, RenderTarget.plain):
@@ -112,8 +92,7 @@ def render(self, target, datum):
if target == RenderTarget.pretty:
if datum in self.colours:
colour = self.colours[datum]
- return Color("{%s}%s{/%s}" % (
- colour, datum, colour))
+ return Color("{%s}%s{/%s}" % (colour, datum, colour))
else:
return datum
else:
@@ -123,98 +102,266 @@ def render(self, target, datum):
class NodePowerColumn(Column):
colours = {
- "on": "autogreen",
- # "off": "", # White.
- "error": "autored",
+ PowerState.ON: "autogreen",
+ # PowerState.OFF: "", # White.
+ PowerState.ERROR: "autored",
}
def render(self, target, data):
if target == RenderTarget.pretty:
if data in self.colours:
colour = self.colours[data]
- return Color("{%s}%s{/%s}" % (
- colour, data.capitalize(), colour))
+ return Color("{%s}%s{/%s}" % (colour, data.value.capitalize(), colour))
else:
- return data.capitalize()
+ return data.value.capitalize()
elif target == RenderTarget.plain:
- return super().render(target, data.capitalize())
+ return super().render(target, data.value.capitalize())
+ else:
+ return super().render(target, data.value)
+
+
+class NodeInterfacesColumn(Column):
+ def render(self, target, data):
+ count = 0
+ for interface in data:
+ if interface.type == InterfaceType.PHYSICAL:
+ count += 1
+ return super().render(target, "%d physical" % count)
+
+
+class NodeOwnerColumn(Column):
+ def render(self, target, data):
+ if data is None:
+ return super().render(target, "(none)")
else:
+ return super().render(target, data.username)
+
+
+class NodeImageColumn(Column):
+ def render(self, target, data):
+ if data:
return super().render(target, data)
+ else:
+ return super().render(target, "(none)")
+
+
+class NodeZoneColumn(Column):
+ def render(self, target, data):
+ return super().render(target, data.name)
+
+
+class NodeResourcePoolColumn(Column):
+ 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__(
+ Column("hostname", "Hostname"), NodeTypeColumn("node_type", "Type")
+ )
+ def get_rows(self, target, nodes):
+ data = ((node.hostname, node.node_type) for node in nodes)
+ return sorted(data, key=itemgetter(0))
+
+
+class MachinesTable(Table):
def __init__(self):
super().__init__(
- NodeTypeColumn("type", ""),
Column("hostname", "Hostname"),
- Column("system_id", "System ID"),
- NodeArchitectureColumn("architecture", "Architecture"),
+ NodePowerColumn("power", "Power"),
+ NodeStatusNameColumn("status", "Status"),
+ NodeOwnerColumn("owner", "Owner"),
+ NodeArchitectureColumn("architecture", "Arch"),
NodeCPUsColumn("cpus", "#CPUs"),
NodeMemoryColumn("memory", "RAM"),
+ NodeResourcePoolColumn("pool", "Resource pool"),
+ NodeZoneColumn("zone", "Zone"),
+ )
+
+ def get_rows(self, target, machines):
+ data = (
+ (
+ machine.hostname,
+ machine.power_state,
+ machine.status_name,
+ machine.owner,
+ machine.architecture,
+ machine.cpus,
+ machine.memory,
+ machine.pool,
+ machine.zone,
+ )
+ for machine in machines
+ )
+ return sorted(data, key=itemgetter(0))
+
+
+class MachineDetail(DetailTable):
+ def __init__(self, with_type=False):
+ self.with_type = with_type
+ columns = [
+ Column("hostname", "Hostname"),
NodeStatusNameColumn("status", "Status"),
+ NodeImageColumn("image", "Image"),
NodePowerColumn("power", "Power"),
+ Column("power_type", "Power Type"),
+ NodeArchitectureColumn("architecture", "Arch"),
+ NodeCPUsColumn("cpus", "#CPUs"),
+ NodeMemoryColumn("memory", "RAM"),
+ NodeInterfacesColumn("interfaces", "Interfaces"),
+ Column("ip_addresses", "IP addresses"),
+ NodeResourcePoolColumn("pool", "Resource pool"),
+ NodeZoneColumn("zone", "Zone"),
+ NodeOwnerColumn("owner", "Owner"),
+ NodeTagsColumn("tags", "Tags"),
+ ]
+ if with_type:
+ columns.insert(1, NodeTypeColumn("node_type", "Type"))
+ super().__init__(*columns)
+
+ def get_rows(self, target, machine):
+ data = [
+ machine.hostname,
+ machine.status_name,
+ machine.distro_series,
+ machine.power_state,
+ machine.power_type,
+ machine.architecture,
+ machine.cpus,
+ machine.memory,
+ machine.interfaces,
+ [
+ link.ip_address
+ for interface in machine.interfaces
+ for link in interface.links
+ if link.ip_address
+ ],
+ machine.pool,
+ machine.zone,
+ machine.owner,
+ machine.tags,
+ ]
+ if self.with_type:
+ data.insert(1, machine.node_type)
+ return data
+
+
+class DevicesTable(Table):
+ def __init__(self):
+ super().__init__(
+ Column("hostname", "Hostname"),
+ NodeOwnerColumn("owner", "Owner"),
+ Column("ip_addresses", "IP addresses"),
)
- @classmethod
- def data_for(cls, node):
- if isinstance(node, Device):
- return (
- NodeTypeColumn.DEVICE,
- node.hostname,
- node.system_id,
- "-",
- None,
- None,
- "-",
- "-",
- )
- elif isinstance(node, Machine):
- return (
- NodeTypeColumn.MACHINE,
- node.hostname,
- node.system_id,
- node.architecture,
- node.cpus,
- node.memory,
- node.status_name,
- node.power_state,
+ def get_rows(self, target, devices):
+ data = (
+ (
+ device.hostname,
+ device.owner,
+ [
+ link.ip_address
+ for interface in device.interfaces
+ for link in interface.links
+ if link.ip_address
+ ],
)
- elif isinstance(node, RackController):
- return (
- NodeTypeColumn.RACK,
- node.hostname,
- node.system_id,
- node.architecture,
- node.cpus,
- node.memory,
- node.status_name,
- node.power_state,
+ for device in devices
+ )
+ return sorted(data, key=itemgetter(0))
+
+
+class DeviceDetail(DetailTable):
+ def __init__(self, with_type=False):
+ self.with_type = with_type
+ columns = [
+ Column("hostname", "Hostname"),
+ NodeInterfacesColumn("interfaces", "Interfaces"),
+ Column("ip_addresses", "IP addresses"),
+ NodeOwnerColumn("owner", "Owner"),
+ Column("tags", "Tags"),
+ ]
+ if with_type:
+ columns.insert(1, NodeTypeColumn("node_type", "Type"))
+ super().__init__(*columns)
+
+ def get_rows(self, target, device):
+ data = [
+ device.hostname,
+ device.interfaces,
+ [
+ link.ip_address
+ for interface in device.interfaces
+ for link in interface.links
+ if link.ip_address
+ ],
+ device.owner,
+ device.tags,
+ ]
+ if self.with_type:
+ data.insert(1, device.node_type)
+ return data
+
+
+class ControllersTable(Table):
+ def __init__(self):
+ super().__init__(
+ Column("hostname", "Hostname"),
+ NodeTypeColumn("node_type", "Type"),
+ NodeArchitectureColumn("architecture", "Arch"),
+ NodeCPUsColumn("cpus", "#CPUs"),
+ NodeMemoryColumn("memory", "RAM"),
+ )
+
+ def get_rows(self, target, controllers):
+ data = (
+ (
+ controller.hostname,
+ controller.node_type,
+ controller.architecture,
+ controller.cpus,
+ controller.memory,
)
- # elif isinstance(node, RegionController):
- # return (
- # "∷",
- # node.hostname,
- # node.system_id,
- # node.architecture,
- # node.cpus,
- # node.memory,
- # node.status_name,
- # node.power_state,
- # )
- else:
- raise TypeError(
- "Cannot extract data from %r (%s)"
- % (node, type(node).__name__))
+ for controller in controllers
+ )
+ return sorted(data, key=itemgetter(0))
- def render(self, target, nodes):
- data = map(self.data_for, nodes)
- data = sorted(data, key=itemgetter(0, 1))
- return super().render(target, data)
+class ControllerDetail(DetailTable):
+ def __init__(self):
+ super().__init__(
+ Column("hostname", "Hostname"),
+ NodeTypeColumn("node_type", "Type"),
+ NodeArchitectureColumn("architecture", "Arch"),
+ NodeCPUsColumn("cpus", "#CPUs"),
+ NodeMemoryColumn("memory", "RAM"),
+ NodeInterfacesColumn("interfaces", "Interfaces"),
+ NodeZoneColumn("zone", "Zone"),
+ )
+
+ def get_rows(self, target, controller):
+ return (
+ controller.hostname,
+ controller.node_type,
+ controller.architecture,
+ controller.cpus,
+ controller.memory,
+ controller.interfaces,
+ controller.zone,
+ )
-class TagsTable(Table):
+class TagsTable(Table):
def __init__(self):
super().__init__(
Column("name", "Tag name"),
@@ -223,26 +370,20 @@ def __init__(self):
Column("comment", "Comment"),
)
- def render(self, target, tags):
+ def get_rows(self, target, tags):
data = (
- (tag.name, tag.definition, tag.kernel_opts, tag.comment)
- for tag in tags
+ (tag.name, tag.definition, tag.kernel_opts, tag.comment) for tag in tags
)
- data = sorted(data, key=itemgetter(0))
- return super().render(target, data)
+ return sorted(data, key=itemgetter(0))
class FilesTable(Table):
-
def __init__(self):
- super().__init__(
- Column("filename", "File name"),
- )
+ super().__init__(Column("filename", "File name"))
- def render(self, target, files):
+ def get_rows(self, target, files):
data = ((f.filename,) for f in files)
- data = sorted(data, key=itemgetter(0))
- return super().render(target, data)
+ return sorted(data, key=itemgetter(0))
class UserIsAdminColumn(Column):
@@ -260,7 +401,6 @@ def render(self, target, is_admin):
class UsersTable(Table):
-
def __init__(self):
super().__init__(
Column("username", "User name"),
@@ -268,23 +408,12 @@ def __init__(self):
UserIsAdminColumn("is_admin", "Admin?"),
)
- def render(self, target, users):
+ def get_rows(self, target, users):
data = ((user.username,) for user in users)
- data = sorted(data, key=itemgetter(0))
- return super().render(target, data)
-
+ return sorted(data, key=itemgetter(0))
-class ProfileAnonymousColumn(Column):
-
- def render(self, target, is_anonymous):
- if target in (RenderTarget.pretty, RenderTarget.plain):
- return "Yes" if is_anonymous else "No"
- else:
- return super().render(target, is_anonymous)
-
-
-class ProfileDefaultColumn(Column):
+class ProfileActiveColumn(Column):
def render(self, target, is_anonymous):
if target is RenderTarget.pretty:
return "✓" if is_anonymous else " "
@@ -295,22 +424,356 @@ def render(self, target, is_anonymous):
class ProfilesTable(Table):
-
def __init__(self):
super().__init__(
- Column("name", "Profile name"),
+ Column("name", "Profile"),
Column("url", "URL"),
- ProfileAnonymousColumn("is_anonymous", "Anonymous?"),
- ProfileDefaultColumn("is_default", "Default?"),
+ ProfileActiveColumn("is_default", "Active"),
)
- def render(self, target, profiles):
+ def get_rows(self, target, profiles):
default = profiles.default
default_name = None if default is None else default.name
data = (
- (profile.name, profile.url, (profile.credentials is None),
- (profile.name == default_name))
+ (profile.name, profile.url, (profile.name == default_name))
for profile in (profiles.load(name) for name in profiles)
)
- data = sorted(data, key=itemgetter(0))
+ return sorted(data, key=itemgetter(0))
+
+
+class VIDColumn(Column):
+ def render(self, target, data):
+ vlan, vlans = data, data._data["fabric"].vlans
+ vlans = sorted(vlans, key=attrgetter("id"))
+ if vlans[0] == vlan:
+ if vlan.vid == 0:
+ data = "untagged"
+ else:
+ data = "untagged (%d)" % vlan.vid
+ else:
+ data = vlan.vid
return super().render(target, data)
+
+
+class DHCPColumn(Column):
+ def render(self, target, vlan):
+ if vlan.dhcp_on:
+ if vlan.primary_rack:
+ if vlan.secondary_rack:
+ text = "HA Enabled"
+ else:
+ text = "Enabled"
+ if target == RenderTarget.pretty:
+ text = Color("{autogreen}%s{/autogreen}") % text
+ elif vlan.relay_vlan:
+ text = "Relayed via %s.%s" % (vlan.fabric.name, vlan.vid)
+ if target == RenderTarget.pretty:
+ text = Color("{autoblue}%s{/autoblue}") % text
+ else:
+ text = "Disabled"
+ return super().render(target, text)
+
+
+class SpaceNameColumn(Column):
+ def render(self, target, space):
+ name = space.name
+ if name == "undefined":
+ name = "(undefined)"
+ return super().render(target, name)
+
+
+class SubnetNameColumn(Column):
+ def render(self, target, subnet):
+ name = subnet.cidr
+ if subnet.name and subnet.name != name:
+ name = "%s (%s)" % (name, subnet.name)
+ return super().render(target, name)
+
+
+class SubnetActiveColumn(Column):
+ def render(self, target, active):
+ if active:
+ text = "Active"
+ else:
+ text = "Disabled"
+ if target == RenderTarget.pretty and active:
+ text = Color("{autogreen}Active{/autogreen}")
+ return super().render(target, text)
+
+
+class SubnetRDNSModeColumn(Column):
+ def render(self, target, mode):
+ if mode == RDNSMode.DISABLED:
+ text = "Disabled"
+ else:
+ if mode == RDNSMode.ENABLED:
+ text = "Enabled"
+ elif mode == RDNSMode.RFC2317:
+ text = "Enabled w/ RFC 2317"
+ if target == RenderTarget.pretty:
+ text = Color("{autogreen}%s{/autogreen}") % text
+ return super().render(target, text)
+
+
+class SubnetsTable(Table):
+ def __init__(self, *, visible_columns=None, fabrics=None):
+ self.fabrics = fabrics
+ super().__init__(
+ SubnetNameColumn("name", "Subnet"),
+ VIDColumn("vid", "VID"),
+ Column("fabric", "Fabric"),
+ SpaceNameColumn("space", "Space"),
+ visible_columns=visible_columns,
+ )
+
+ def get_vlan(self, vlan):
+ fabric = self.get_fabric(vlan.fabric)
+ for fabric_vlan in fabric.vlans:
+ if fabric_vlan.id == vlan.id:
+ return fabric_vlan
+
+ def get_fabric(self, unloaded_fabric):
+ for fabric in self.fabrics:
+ if fabric.id == unloaded_fabric.id:
+ return fabric
+
+ def get_rows(self, target, subnets):
+ return (
+ (
+ subnet,
+ self.get_vlan(subnet.vlan),
+ self.get_fabric(subnet.vlan.fabric).name,
+ subnet.vlan.space,
+ )
+ for subnet in sorted(subnets, key=attrgetter("cidr"))
+ )
+
+
+class SubnetDetail(DetailTable):
+ def __init__(self, *, fabrics=None):
+ self.fabrics = fabrics
+ super().__init__(
+ Column("name", "Name"),
+ Column("cidr", "CIDR"),
+ Column("gateway_ip", "Gateway IP"),
+ Column("dns", "DNS"),
+ VIDColumn("vid", "VID"),
+ Column("fabric", "Fabric"),
+ SpaceNameColumn("space", "Space"),
+ SubnetActiveColumn("managed", "Managed allocation"),
+ SubnetActiveColumn("allow_proxy", "Allow proxy"),
+ SubnetActiveColumn("active_discovery", "Active discovery"),
+ SubnetRDNSModeColumn("rdns_mode", "Reverse DNS mode"),
+ )
+
+ def get_vlan(self, vlan):
+ fabric = self.get_fabric(vlan.fabric)
+ for fabric_vlan in fabric.vlans:
+ if fabric_vlan.id == vlan.id:
+ return fabric_vlan
+
+ def get_fabric(self, unloaded_fabric):
+ for fabric in self.fabrics:
+ if fabric.id == unloaded_fabric.id:
+ return fabric
+
+ def get_rows(self, target, subnet):
+ return (
+ subnet.name,
+ subnet.cidr,
+ subnet.gateway_ip,
+ subnet.dns_servers,
+ self.get_vlan(subnet.vlan),
+ self.get_fabric(subnet.vlan.fabric).name,
+ subnet.vlan.space,
+ subnet.managed,
+ subnet.allow_proxy,
+ subnet.active_discovery,
+ subnet.rdns_mode,
+ )
+
+
+class VlansTable(Table):
+ def __init__(self, *, visible_columns=None, fabrics=None, subnets=None):
+ self.subnets = subnets
+ super().__init__(
+ VIDColumn("vid", "VID"),
+ DHCPColumn("dhcp", "DHCP"),
+ SpaceNameColumn("space", "Space"),
+ NestedTableColumn(
+ "subnets",
+ "Subnets",
+ SubnetsTable,
+ None,
+ {"visible_columns": ("name",), "fabrics": fabrics},
+ ),
+ visible_columns=visible_columns,
+ )
+
+ def get_subnets(self, vlan):
+ """Return the subnets for the `vlan`."""
+ return vlan._origin.Subnets(
+ [subnet for subnet in self.subnets if subnet.vlan.id == vlan.id]
+ )
+
+ def get_rows(self, target, vlans):
+ return (
+ (vlan, vlan, vlan.space, self.get_subnets(vlan))
+ for vlan in sorted(vlans, key=attrgetter("vid"))
+ )
+
+
+class VlanDetail(DetailTable):
+ def __init__(self, *, fabrics=None, subnets=None):
+ self.fabrics = fabrics
+ self.subnets = subnets
+ super().__init__(
+ VIDColumn("vid", "VID"),
+ Column("name", "Name"),
+ Column("fabric", "Fabric"),
+ Column("mtu", "MTU"),
+ DHCPColumn("dhcp", "DHCP"),
+ Column("primary_rack", "Primary rack"),
+ Column("secondary_rack", "Secondary rack"),
+ SpaceNameColumn("space", "Space"),
+ NestedTableColumn(
+ "subnets",
+ "Subnets",
+ SubnetsTable,
+ None,
+ {"visible_columns": ("name",), "fabrics": fabrics},
+ ),
+ )
+
+ def get_fabric(self, vlan):
+ for fabric in self.fabrics:
+ if fabric.id == vlan.fabric.id:
+ return fabric
+
+ def get_subnets(self, vlan):
+ """Return the subnets for the `vlan`."""
+ return vlan._origin.Subnets(
+ [subnet for subnet in self.subnets if subnet.vlan.id == vlan.id]
+ )
+
+ def get_rows(self, target, vlan):
+ primary_rack = vlan.primary_rack
+ if primary_rack is not None:
+ primary_rack.refresh()
+ secondary_rack = vlan.secondary_rack
+ if secondary_rack is not None:
+ secondary_rack.refresh()
+ return (
+ vlan,
+ vlan.name,
+ self.get_fabric(vlan).name,
+ vlan.mtu,
+ vlan,
+ primary_rack.hostname if primary_rack else None,
+ secondary_rack.hostname if secondary_rack else None,
+ vlan.space,
+ self.get_subnets(vlan),
+ )
+
+
+class FabricsTable(Table):
+ def __init__(self, *, visible_columns=None, subnets=None):
+ super().__init__(
+ Column("name", "Fabric"),
+ NestedTableColumn("vlans", "VLANs", VlansTable, None, {"subnets": subnets}),
+ visible_columns=visible_columns,
+ )
+
+ def get_rows(self, target, fabrics):
+ self["vlans"].table_kwargs["fabrics"] = fabrics
+ return (
+ (fabric.name, fabric.vlans)
+ for fabric in sorted(fabrics, key=attrgetter("id"))
+ )
+
+
+class FabricDetail(DetailTable):
+ def __init__(self, *, fabrics=None, subnets=None):
+ super().__init__(
+ Column("name", "Name"),
+ NestedTableColumn(
+ "vlans",
+ "VLANs",
+ VlansTable,
+ None,
+ {"fabrics": fabrics, "subnets": subnets},
+ ),
+ )
+
+ def get_rows(self, target, fabric):
+ return (fabric.name, fabric.vlans)
+
+
+class SpacesTable(Table):
+ def __init__(self, *, visible_columns=None, fabrics=None, subnets=None):
+ self.fabrics = fabrics
+ super().__init__(
+ SpaceNameColumn("name", "Space"),
+ NestedTableColumn(
+ "vlans",
+ "VLANs",
+ VlansTable,
+ None,
+ {
+ "visible_columns": ("vid", "dhcp", "subnets"),
+ "fabrics": fabrics,
+ "subnets": subnets,
+ },
+ ),
+ visible_columns=visible_columns,
+ )
+
+ def get_fabric(self, vlan):
+ for fabric in self.fabrics:
+ for fabric_vlan in fabric.vlans:
+ if fabric_vlan.id == vlan.id:
+ return fabric
+
+ def get_vlans(self, vlans):
+ for vlan in vlans:
+ vlan._data["fabric"] = self.get_fabric(vlan)
+ return vlans
+
+ def get_rows(self, target, spaces):
+ return (
+ (space, self.get_vlans(space.vlans))
+ for space in sorted(spaces, key=attrgetter("name"))
+ )
+
+
+class SpaceDetail(DetailTable):
+ def __init__(self, *, fabrics=None, subnets=None):
+ self.fabrics = fabrics
+ super().__init__(
+ SpaceNameColumn("name", "Space"),
+ NestedTableColumn(
+ "vlans",
+ "VLANs",
+ VlansTable,
+ None,
+ {
+ "visible_columns": ("vid", "dhcp", "subnets"),
+ "fabrics": fabrics,
+ "subnets": subnets,
+ },
+ ),
+ )
+
+ def get_fabric(self, vlan):
+ for fabric in self.fabrics:
+ for fabric_vlan in fabric.vlans:
+ if fabric_vlan.id == vlan.id:
+ return fabric
+
+ def get_vlans(self, vlans):
+ for vlan in vlans:
+ vlan._data["fabric"] = self.get_fabric(vlan)
+ return vlans
+
+ def get_rows(self, target, space):
+ return (space, self.get_vlans(space.vlans))
diff --git a/maas/client/flesh/tabular.py b/maas/client/flesh/tabular.py
index 7a2539b4..a59c097d 100644
--- a/maas/client/flesh/tabular.py
+++ b/maas/client/flesh/tabular.py
@@ -1,12 +1,10 @@
"""Helpers to assemble and render tabular data."""
-__all__ = [
- "Column",
- "RenderTarget",
- "Table",
-]
+__all__ = ["Column", "RenderTarget", "Table"]
+from abc import ABCMeta, abstractmethod
import collections
+from collections.abc import Iterable
import csv
import enum
from io import StringIO
@@ -35,78 +33,260 @@ def __str__(self):
return self.value
-class Table:
-
- def __init__(self, *columns):
+class Table(metaclass=ABCMeta):
+ def __init__(self, *columns, visible_columns=None):
super(Table, self).__init__()
self.columns = collections.OrderedDict(
- (column.name, column) for column in columns)
+ (column.name, column) for column in columns
+ )
+ if visible_columns is None:
+ self.visible_columns = collections.OrderedDict(self.columns.items())
+ else:
+ self.visible_columns = collections.OrderedDict(
+ (column.name, column)
+ for column in columns
+ if column.name in visible_columns
+ )
def __getitem__(self, name):
return self.columns[name]
+ @abstractmethod
+ def get_rows(self, target, data):
+ """Get the rows for the table."""
+
+ def _filter_rows(self, rows):
+ """Filter `rows` based on the visible columns."""
+ filtered_rows = []
+ for row in rows:
+ filtered_row = []
+ for idx, name in enumerate(self.columns.keys()):
+ if name in self.visible_columns:
+ filtered_row.append(row[idx])
+ filtered_rows.append(filtered_row)
+ return filtered_rows
+
def render(self, target, data):
- columns = self.columns.values()
- rows = [
- [column.render(target, datum)
- for datum, column in zip(row, columns)]
- for row in data
- ]
+ """Render the table."""
+ rows = self.get_rows(target, data)
+ rows = self._filter_rows(rows)
renderer = getattr(self, "_render_%s" % target.name, None)
if renderer is None:
- raise ValueError(
- "Cannot render %r for %s." % (self.value, target))
+ raise ValueError("Cannot render %r for %s." % (self.value, target))
else:
- return renderer(columns, rows)
+ return renderer(rows)
- def _render_plain(self, columns, rows):
- rows.insert(0, [column.title for column in columns])
+ def _flatten_columns(self, columns):
+ cols = []
+ for _, column in columns.items():
+ if isinstance(column, NestedTableColumn):
+ cols.extend(self._flatten_columns(column.get_columns()))
+ else:
+ cols.append(column)
+ return cols
+
+ def _compute_rows(self, target, data, duplicate=False):
+ columns = self.visible_columns.values()
+ computed = []
+ for row_data in data:
+ row = []
+ rows = [row]
+ for datum, column in zip(row_data, columns):
+ if isinstance(column, NestedTableColumn):
+ nested_rows = column.get_rows(target, datum, duplicate=duplicate)
+ orig_row = list(row)
+ row.extend(nested_rows[0])
+ for nested_row in nested_rows[1:]:
+ new_row = list(orig_row)
+ if not duplicate:
+ for idx in range(len(new_row)):
+ new_row[idx] = ""
+ new_row.extend(nested_row)
+ rows.append(new_row)
+ else:
+ row.append(column.render(target, datum))
+ computed.extend(rows)
+ return computed
+
+ def _render_plain(self, data):
+ rows = self._compute_rows(RenderTarget.plain, data)
+ rows.insert(
+ 0, [column.title for column in self._flatten_columns(self.visible_columns)]
+ )
return terminaltables.AsciiTable(rows).table
- def _render_pretty(self, columns, rows):
- rows.insert(0, [column.title for column in columns])
+ def _render_pretty(self, data):
+ rows = self._compute_rows(RenderTarget.pretty, data)
+ rows.insert(
+ 0, [column.title for column in self._flatten_columns(self.visible_columns)]
+ )
return terminaltables.SingleTable(rows).table
- def _render_yaml(self, columns, rows):
- return yaml.safe_dump({
- "columns": [
- {"name": column.name, "title": column.title}
- for column in columns
- ],
- "data": [
- {column.name: datum
- for column, datum in zip(columns, row)}
- for row in rows
- ],
- }, default_flow_style=False).rstrip(linesep)
-
- def _render_json(self, columns, rows):
- return json.dumps({
- "columns": [
- {"name": column.name, "title": column.title}
- for column in columns
- ],
- "data": [
- {column.name: datum
- for column, datum in zip(columns, row)}
- for row in rows
- ],
- })
-
- def _render_csv(self, columns, rows):
+ def _render_yaml(self, data):
+ columns = self.visible_columns.values()
+ rows = [
+ [
+ column.render(RenderTarget.yaml, datum)
+ for datum, column in zip(row, columns)
+ ]
+ for row in data
+ ]
+ return yaml.safe_dump(
+ {
+ "columns": [
+ {"name": column.name, "title": column.title} for column in columns
+ ],
+ "data": [
+ {column.name: datum for column, datum in zip(columns, row)}
+ for row in rows
+ ],
+ },
+ default_flow_style=False,
+ ).rstrip(linesep)
+
+ def _render_json(self, data):
+ columns = self.visible_columns.values()
+ rows = [
+ [
+ column.render(RenderTarget.json, datum)
+ for datum, column in zip(row, columns)
+ ]
+ for row in data
+ ]
+ return json.dumps(
+ {
+ "columns": [
+ {"name": column.name, "title": column.title} for column in columns
+ ],
+ "data": [
+ {column.name: datum for column, datum in zip(columns, row)}
+ for row in rows
+ ],
+ }
+ )
+
+ def _render_csv(self, data):
output = StringIO()
writer = csv.writer(output)
- writer.writerow([column.name for column in columns])
- writer.writerows(rows)
+ writer.writerow(
+ [column.name for column in self._flatten_columns(self.visible_columns)]
+ )
+ writer.writerows(self._compute_rows(RenderTarget.csv, data, duplicate=True))
return output.getvalue().rstrip(linesep)
def __repr__(self):
- return "<%s [%s]>" % (
- self.__class__.__name__, " ".join(self.columns))
+ return "<%s [%s]>" % (self.__class__.__name__, " ".join(self.visible_columns))
-class Column:
+class DetailTable(Table):
+ def _filter_rows(self, rows, visible_columns=None):
+ """Filter `rows` based on the visible columns."""
+ if visible_columns is None:
+ visible_columns = self.visible_columns
+ filtered_row = []
+ for idx, name in enumerate(self.columns.keys()):
+ if name in self.visible_columns:
+ filtered_row.append(rows[idx])
+ return filtered_row
+
+ def render(self, target, data):
+ renderer = getattr(self, "_render_%s" % target.name, None)
+ if renderer is None:
+ raise ValueError("Cannot render %r for %s." % (self.value, target))
+ else:
+ return renderer(data)
+
+ def _split_nested_tables(self):
+ table_columns = collections.OrderedDict(
+ (name, column)
+ for name, column in self.visible_columns.items()
+ if isinstance(column, NestedTableColumn)
+ )
+ data_columns = collections.OrderedDict(
+ (name, column)
+ for name, column in self.visible_columns.items()
+ if not isinstance(column, NestedTableColumn)
+ )
+ return data_columns, table_columns
+
+ def _render_nested_table(self, target, data, column):
+ data_idx = list(self.columns.keys()).index(column.name)
+ data = data[data_idx]
+ table = column.get_table()
+ return table.render(target, data)
+
+ def _render_nested_tables(self, target, data, columns):
+ tables = [
+ self._render_nested_table(target, data, column)
+ for _, column in columns.items()
+ ]
+ if len(tables) > 0:
+ return "\n" + "\n".join(tables)
+ else:
+ return ""
+ def _render_table(self, target, terminaltable, data):
+ columns, table_columns = self._split_nested_tables()
+ all_rows = self.get_rows(target, data)
+ rows = self._filter_rows(all_rows, visible_columns=columns)
+ rows = [
+ column.render(target, datum)
+ for column, datum in zip(columns.values(), rows)
+ ]
+ table = terminaltable(
+ [(column.title, datum) for column, datum in zip(columns.values(), rows)]
+ )
+ table.inner_heading_row_border = False
+ return table.table + self._render_nested_tables(target, all_rows, table_columns)
+
+ def _render_plain(self, data):
+ return self._render_table(RenderTarget.plain, terminaltables.AsciiTable, data)
+
+ def _render_pretty(self, data):
+ return self._render_table(RenderTarget.pretty, terminaltables.SingleTable, data)
+
+ def _render_yaml(self, data):
+ columns = self.visible_columns.values()
+ rows = self.get_rows(RenderTarget.yaml, data)
+ rows = self._filter_rows(rows)
+ rows = [
+ column.render(RenderTarget.yaml, datum)
+ for column, datum in zip(columns, rows)
+ ]
+ return yaml.safe_dump(
+ {column.name: datum for column, datum in zip(columns, rows)},
+ default_flow_style=False,
+ ).rstrip(linesep)
+
+ def _render_json(self, data):
+ columns = self.visible_columns.values()
+ rows = self.get_rows(RenderTarget.json, data)
+ rows = self._filter_rows(rows)
+ rows = [
+ column.render(RenderTarget.json, datum)
+ for column, datum in zip(columns, rows)
+ ]
+ return json.dumps({column.name: datum for column, datum in zip(columns, rows)})
+
+ def _render_csv(self, data):
+ columns, table_columns = self._split_nested_tables()
+ all_rows = self.get_rows(RenderTarget.csv, data)
+ rows = self._filter_rows(all_rows, visible_columns=columns)
+ rows = [
+ column.render(RenderTarget.csv, datum)
+ for column, datum in zip(columns.values(), rows)
+ ]
+ output = StringIO()
+ writer = csv.writer(output)
+ writer.writerows(
+ [(column.name, datum) for column, datum in zip(columns.values(), rows)]
+ )
+ return output.getvalue().rstrip(linesep) + (
+ self._render_nested_tables(RenderTarget.csv, all_rows, table_columns)
+ )
+
+
+class Column:
def __init__(self, name, title=None):
super(Column, self).__init__()
self.name = name
@@ -118,12 +298,17 @@ def render(self, target, datum):
elif target is RenderTarget.json:
return datum
elif target is RenderTarget.csv:
- return datum
+ if isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)):
+ return ",".join(datum)
+ else:
+ return datum
elif target is RenderTarget.plain:
if datum is None:
return ""
elif isinstance(datum, colorclass.Color):
return datum.value_no_colors
+ elif isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)):
+ return "\n".join(datum)
else:
return str(datum)
elif target is RenderTarget.pretty:
@@ -131,12 +316,63 @@ def render(self, target, datum):
return ""
elif isinstance(datum, colorclass.Color):
return datum
+ elif isinstance(datum, Iterable) and not isinstance(datum, (str, bytes)):
+ return "\n".join(datum)
else:
return str(datum)
else:
- raise ValueError(
- "Cannot render %r for %s" % (datum, target))
+ raise ValueError("Cannot render %r for %s" % (datum, target))
def __repr__(self):
return "<%s name=%s title=%r>" % (
- self.__class__.__name__, self.name, self.title)
+ self.__class__.__name__,
+ self.name,
+ self.title,
+ )
+
+
+class NestedTableColumn(Column):
+ def __init__(
+ self, name, title=None, table=None, table_args=None, table_kwargs=None
+ ):
+ super(NestedTableColumn, self).__init__(name, title=title)
+ self.table = table
+ self.table_args = table_args
+ self.table_kwargs = table_kwargs
+ if table is None:
+ raise ValueError("table is required.")
+
+ def get_table(self):
+ table_args = self.table_args
+ if table_args is None:
+ table_args = []
+ table_kwargs = self.table_kwargs
+ if table_kwargs is None:
+ table_kwargs = {}
+ return self.table(*table_args, **table_kwargs)
+
+ def get_columns(self):
+ return self.get_table().visible_columns
+
+ def get_rows(self, target, data, duplicate=False):
+ table = self.get_table()
+ rows = table.get_rows(target, data)
+ rows = table._filter_rows(rows)
+ rows = table._compute_rows(target, rows, duplicate=duplicate)
+ if len(rows) == 0:
+ # Nested table column must always return one row even if its
+ # an empty row.
+ rows = [[" " for _ in range(len(table.visible_columns))]]
+ return rows
+
+ def render(self, target, datum):
+ table = self.get_table()
+ if target is RenderTarget.yaml:
+ return yaml.safe_load(table.render(target, datum))
+ elif target is RenderTarget.json:
+ return json.loads(table.render(target, datum))
+ else:
+ raise ValueError(
+ "Should not be called on a nested table column, the "
+ "table render should handle this correctly."
+ )
diff --git a/maas/client/flesh/tags.py b/maas/client/flesh/tags.py
index 794c8921..e2498c2a 100644
--- a/maas/client/flesh/tags.py
+++ b/maas/client/flesh/tags.py
@@ -1,23 +1,18 @@
"""Commands for tags."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
-from . import (
- OriginTableCommand,
- tables,
-)
+from . import OriginPagedTableCommand, tables
-class cmd_list_tags(OriginTableCommand):
+class cmd_tags(OriginPagedTableCommand):
"""List tags."""
def execute(self, origin, options, target):
table = tables.TagsTable()
- print(table.render(target, origin.Tags))
+ return table.render(target, origin.Tags.read())
def register(parser):
"""Register profile commands with the given parser."""
- cmd_list_tags.register(parser)
+ cmd_tags.register(parser)
diff --git a/maas/client/flesh/tests/test_flesh.py b/maas/client/flesh/tests/test.py
similarity index 100%
rename from maas/client/flesh/tests/test_flesh.py
rename to maas/client/flesh/tests/test.py
diff --git a/maas/client/flesh/tests/test_controllers.py b/maas/client/flesh/tests/test_controllers.py
new file mode 100644
index 00000000..e5e7e411
--- /dev/null
+++ b/maas/client/flesh/tests/test_controllers.py
@@ -0,0 +1,114 @@
+"""Tests for `maas.client.flesh.controllers`."""
+
+from operator import itemgetter
+import yaml
+
+from .testing import TestCaseWithProfile
+from .. import ArgumentParser, controllers, tabular
+from ...enum import NodeType
+from ...testing import make_name_without_spaces
+from ...viscera.testing import bind
+from ...viscera.controllers import (
+ RackController,
+ RackControllers,
+ RegionController,
+ RegionControllers,
+)
+
+
+def make_origin():
+ """Make origin for controllers."""
+ return bind(RackControllers, RackController, RegionController, RegionControllers)
+
+
+class TestControllers(TestCaseWithProfile):
+ """Tests for `cmd_controllers`."""
+
+ def test_returns_table_with_controllers(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ region_rack_id = make_name_without_spaces()
+ region_rack_hostname = make_name_without_spaces()
+ racks = [
+ {
+ "system_id": region_rack_id,
+ "hostname": region_rack_hostname,
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ "architecture": "amd64/generic",
+ "cpu_count": 2,
+ "memory": 1024,
+ },
+ {
+ "system_id": make_name_without_spaces(),
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ "architecture": "amd64/generic",
+ "cpu_count": 2,
+ "memory": 1024,
+ },
+ ]
+ regions = [
+ {
+ "system_id": region_rack_id,
+ "hostname": region_rack_hostname,
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ "architecture": "amd64/generic",
+ "cpu_count": 2,
+ "memory": 1024,
+ },
+ {
+ "system_id": make_name_without_spaces(),
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ "architecture": "amd64/generic",
+ "cpu_count": 2,
+ "memory": 1024,
+ },
+ ]
+ origin.RackControllers._handler.read.return_value = racks
+ origin.RegionControllers._handler.read.return_value = regions
+ cmd = controllers.cmd_controllers(parser)
+ subparser = controllers.cmd_controllers.register(parser)
+ options = subparser.parse_args([])
+ output = yaml.safe_load(
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ )
+ self.assertEquals(
+ [
+ {"name": "hostname", "title": "Hostname"},
+ {"name": "node_type", "title": "Type"},
+ {"name": "architecture", "title": "Arch"},
+ {"name": "cpus", "title": "#CPUs"},
+ {"name": "memory", "title": "RAM"},
+ ],
+ output["columns"],
+ )
+ controller_output = {
+ controller["hostname"]: {
+ "hostname": controller["hostname"],
+ "node_type": controller["node_type"],
+ "architecture": controller["architecture"],
+ "cpus": controller["cpu_count"],
+ "memory": controller["memory"],
+ }
+ for controller in racks + regions
+ }
+ self.assertEquals(
+ sorted(controller_output.values(), key=itemgetter("hostname")),
+ output["data"],
+ )
+
+ def test_calls_handler_with_hostnames(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ origin.RackControllers._handler.read.return_value = []
+ origin.RegionControllers._handler.read.return_value = []
+ subparser = controllers.cmd_controllers.register(parser)
+ cmd = controllers.cmd_controllers(parser)
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ options = subparser.parse_args(hostnames)
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ origin.RackControllers._handler.read.assert_called_once_with(hostname=hostnames)
+ origin.RegionControllers._handler.read.assert_called_once_with(
+ hostname=hostnames
+ )
diff --git a/maas/client/flesh/tests/test_devices.py b/maas/client/flesh/tests/test_devices.py
new file mode 100644
index 00000000..2b355725
--- /dev/null
+++ b/maas/client/flesh/tests/test_devices.py
@@ -0,0 +1,90 @@
+"""Tests for `maas.client.flesh.devices`."""
+
+from operator import itemgetter
+import yaml
+
+from .testing import TestCaseWithProfile
+from .. import ArgumentParser, devices, tabular
+from ...testing import make_name_without_spaces
+from ...viscera.testing import bind
+from ...viscera.devices import Device, Devices
+from ...viscera.interfaces import Interface, Interfaces, InterfaceLink, InterfaceLinks
+from ...viscera.users import User
+
+
+def make_origin():
+ """Make origin for devices."""
+ return bind(
+ Devices, Device, User, Interfaces, Interface, InterfaceLinks, InterfaceLink
+ )
+
+
+class TestDevices(TestCaseWithProfile):
+ """Tests for `cmd_devices`."""
+
+ def test_returns_table_with_devices(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ devices_objs = [
+ {
+ "hostname": make_name_without_spaces(),
+ "owner": make_name_without_spaces(),
+ "interface_set": [
+ {"links": [{"ip_address": "192.168.122.1"}]},
+ {"links": [{"ip_address": "192.168.122.2"}]},
+ {"links": [{}]},
+ ],
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "owner": make_name_without_spaces(),
+ "interface_set": [
+ {"links": [{"ip_address": "192.168.122.10"}]},
+ {"links": [{"ip_address": "192.168.122.11"}]},
+ {"links": [{}]},
+ ],
+ },
+ ]
+ origin.Devices._handler.read.return_value = devices_objs
+ cmd = devices.cmd_devices(parser)
+ subparser = devices.cmd_devices.register(parser)
+ options = subparser.parse_args([])
+ output = yaml.safe_load(
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ )
+ self.assertEquals(
+ [
+ {"name": "hostname", "title": "Hostname"},
+ {"name": "owner", "title": "Owner"},
+ {"name": "ip_addresses", "title": "IP addresses"},
+ ],
+ output["columns"],
+ )
+ devices_output = sorted(
+ [
+ {
+ "hostname": device["hostname"],
+ "owner": device["owner"] if device["owner"] else "(none)",
+ "ip_addresses": [
+ link["ip_address"]
+ for nic in device["interface_set"]
+ for link in nic["links"]
+ if link.get("ip_address")
+ ],
+ }
+ for device in devices_objs
+ ],
+ key=itemgetter("hostname"),
+ )
+ self.assertEquals(devices_output, output["data"])
+
+ def test_calls_handler_with_hostnames(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ origin.Devices._handler.read.return_value = []
+ subparser = devices.cmd_devices.register(parser)
+ cmd = devices.cmd_devices(parser)
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ options = subparser.parse_args(hostnames)
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ origin.Devices._handler.read.assert_called_once_with(hostname=hostnames)
diff --git a/maas/client/flesh/tests/test_machines.py b/maas/client/flesh/tests/test_machines.py
new file mode 100644
index 00000000..6e3c9412
--- /dev/null
+++ b/maas/client/flesh/tests/test_machines.py
@@ -0,0 +1,165 @@
+"""Tests for `maas.client.flesh.machines`."""
+
+from functools import partial
+from operator import itemgetter
+import yaml
+
+from .testing import TestCaseWithProfile
+from .. import ArgumentParser, machines, tabular
+from ...enum import NodeStatus, PowerState
+from ...testing import make_name_without_spaces
+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, Tag, Tags)
+
+
+class TestMachines(TestCaseWithProfile):
+ """Tests for `cmd_machines`."""
+
+ def test_returns_table_with_machines(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ machine_objs = [
+ {
+ "hostname": make_name_without_spaces(),
+ "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"},
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "architecture": "i386/generic",
+ "status": NodeStatus.DEPLOYED.value,
+ "status_name": NodeStatus.DEPLOYED.name,
+ "owner": make_name_without_spaces(),
+ "power_state": PowerState.ON.value,
+ "cpu_count": 4,
+ "memory": 4096,
+ "pool": {"id": 2, "name": "pool2", "description": "pool2"},
+ "zone": {"id": 2, "name": "zone2", "description": "zone2"},
+ },
+ ]
+ origin.Machines._handler.read.return_value = machine_objs
+ cmd = machines.cmd_machines(parser)
+ subparser = machines.cmd_machines.register(parser)
+ options = subparser.parse_args([])
+ output = yaml.safe_load(
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ )
+ self.assertEquals(
+ [
+ {"name": "hostname", "title": "Hostname"},
+ {"name": "power", "title": "Power"},
+ {"name": "status", "title": "Status"},
+ {"name": "owner", "title": "Owner"},
+ {"name": "architecture", "title": "Arch"},
+ {"name": "cpus", "title": "#CPUs"},
+ {"name": "memory", "title": "RAM"},
+ {"name": "pool", "title": "Resource pool"},
+ {"name": "zone", "title": "Zone"},
+ ],
+ output["columns"],
+ )
+ machines_output = sorted(
+ [
+ {
+ "hostname": machine["hostname"],
+ "power": machine["power_state"],
+ "status": machine["status_name"],
+ "owner": machine["owner"] if machine["owner"] else "(none)",
+ "architecture": machine["architecture"],
+ "cpus": machine["cpu_count"],
+ "memory": machine["memory"],
+ "pool": machine["pool"]["name"],
+ "zone": machine["zone"]["name"],
+ }
+ for machine in machine_objs
+ ],
+ key=itemgetter("hostname"),
+ )
+ self.assertEquals(machines_output, output["data"])
+
+ def test_calls_handler_with_hostnames(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ origin.Machines._handler.read.return_value = []
+ subparser = machines.cmd_machines.register(parser)
+ cmd = machines.cmd_machines(parser)
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ 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 |
++---------------+-------------+""",
+ )
diff --git a/maas/client/flesh/tests/test_nodes.py b/maas/client/flesh/tests/test_nodes.py
new file mode 100644
index 00000000..5dce2604
--- /dev/null
+++ b/maas/client/flesh/tests/test_nodes.py
@@ -0,0 +1,79 @@
+"""Tests for `maas.client.flesh.nodes`."""
+
+from operator import itemgetter
+import yaml
+
+from .testing import TestCaseWithProfile
+from .. import ArgumentParser, nodes, tabular
+from ...enum import NodeType
+from ...testing import make_name_without_spaces
+from ...viscera.testing import bind
+from ...viscera.nodes import Node, Nodes
+
+
+def make_origin():
+ """Make origin for nodes."""
+ return bind(Nodes, Node)
+
+
+class TestNodes(TestCaseWithProfile):
+ """Tests for `cmd_nodes`."""
+
+ def test_returns_table_with_nodes(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ node_obj = [
+ {
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.MACHINE.value,
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.DEVICE.value,
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ },
+ {
+ "hostname": make_name_without_spaces(),
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ },
+ ]
+ origin.Nodes._handler.read.return_value = node_obj
+ cmd = nodes.cmd_nodes(parser)
+ subparser = nodes.cmd_nodes.register(parser)
+ options = subparser.parse_args([])
+ output = yaml.safe_load(
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ )
+ self.assertEquals(
+ [
+ {"name": "hostname", "title": "Hostname"},
+ {"name": "node_type", "title": "Type"},
+ ],
+ output["columns"],
+ )
+ nodes_output = sorted(
+ [
+ {"hostname": node["hostname"], "node_type": node["node_type"]}
+ for node in node_obj
+ ],
+ key=itemgetter("hostname"),
+ )
+ self.assertEquals(nodes_output, output["data"])
+
+ def test_calls_handler_with_hostnames(self):
+ origin = make_origin()
+ parser = ArgumentParser()
+ origin.Nodes._handler.read.return_value = []
+ subparser = nodes.cmd_nodes.register(parser)
+ cmd = nodes.cmd_nodes(parser)
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ options = subparser.parse_args(hostnames)
+ cmd.execute(origin, options, target=tabular.RenderTarget.yaml)
+ origin.Nodes._handler.read.assert_called_once_with(hostname=hostnames)
diff --git a/maas/client/flesh/tests/test_profiles.py b/maas/client/flesh/tests/test_profiles.py
index 134ca19e..d71e7270 100644
--- a/maas/client/flesh/tests/test_profiles.py
+++ b/maas/client/flesh/tests/test_profiles.py
@@ -1,32 +1,67 @@
"""Tests for `maas.client.flesh.profiles`."""
-__all__ = []
-
+from argparse import ArgumentParser
from io import StringIO
import sys
from textwrap import dedent
+from unittest.mock import call
from .. import profiles
-from ...testing import TestCase
+from ...bones.helpers import MacaroonLoginNotSupported
+from ...testing import AsyncCallableMock, TestCase
from ...utils.tests.test_profiles import make_profile
-class TestLoginBase(TestCase):
- """Tests for `cmd_login_base`."""
+class TestLogin(TestCase):
+ """Tests for `cmd_login`."""
+
+ def test_login_no_macaroons_prompts_user_pass(self):
+ profile = make_profile()
+
+ stdout = self.patch(sys, "stdout", StringIO())
+ mock_read_input = self.patch(profiles, "read_input")
+ mock_read_input.side_effect = ["username", "password"]
+ mock_login = AsyncCallableMock(side_effect=[MacaroonLoginNotSupported, profile])
+ self.patch(profiles.helpers, "login", mock_login)
+
+ parser = ArgumentParser()
+ cmd = profiles.cmd_login(parser)
+ options = parser.parse_args(["http://maas.example"])
+ cmd(options)
+ mock_login.assert_has_calls(
+ [
+ call(
+ "http://maas.example/api/2.0/",
+ anonymous=False,
+ insecure=False,
+ username=None,
+ password=None,
+ ),
+ call(
+ "http://maas.example/api/2.0/",
+ insecure=False,
+ username="username",
+ password="password",
+ ),
+ ]
+ )
+ self.assertIn("Congratulations!", stdout.getvalue())
def test_print_whats_next(self):
profile = make_profile()
stdout = self.patch(sys, "stdout", StringIO())
- profiles.cmd_login_base.print_whats_next(profile)
- expected = dedent("""\
+ profiles.cmd_login.print_whats_next(profile)
+ expected = dedent(
+ """\
Congratulations! You are logged in to the MAAS
server at {profile.url} with the profile name
{profile.name}.
For help with the available commands, try:
- maas --help
+ maas help
- """).format(profile=profile)
+ """
+ ).format(profile=profile)
observed = stdout.getvalue()
self.assertDocTestMatches(expected, observed)
diff --git a/maas/client/flesh/tests/test_shell.py b/maas/client/flesh/tests/test_shell.py
index a080c3f1..43787148 100644
--- a/maas/client/flesh/tests/test_shell.py
+++ b/maas/client/flesh/tests/test_shell.py
@@ -1,33 +1,25 @@
"""Tests for `maas.client.flesh.shell`."""
-__all__ = []
-
import random
+import sys
+
+from testtools.matchers import Contains, Equals, Is
-from testtools.matchers import (
- Contains,
- Equals,
- Is,
-)
-
-from .. import (
- ArgumentParser,
- shell,
-)
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
+from .. import ArgumentParser, shell
+from ...testing import make_name_without_spaces, TestCase
from .testing import capture_parse_error
class TestShell(TestCase):
"""Tests for `cmd_shell`."""
- def test_offers_profile_name_option_when_no_profiles_exist(self):
+ def setUp(self):
+ super(TestShell, self).setUp()
+ # Start with no profiles, and no profile default.
self.patch(shell.cmd_shell, "profile_name_choices", ())
self.patch(shell.cmd_shell, "profile_name_default", None)
+ def test_offers_profile_name_option_when_no_profiles_exist(self):
parser = ArgumentParser()
subparser = shell.cmd_shell.register(parser)
@@ -42,13 +34,12 @@ def test_offers_profile_name_option_when_no_profiles_exist(self):
def test_offers_profile_name_option_when_profiles_exist(self):
profile_name_choices = tuple(
- make_name_without_spaces("profile-name") for _ in range(5))
+ make_name_without_spaces("profile-name") for _ in range(5)
+ )
profile_name_default = random.choice(profile_name_choices)
- self.patch(
- shell.cmd_shell, "profile_name_choices", profile_name_choices)
- self.patch(
- shell.cmd_shell, "profile_name_default", profile_name_default)
+ self.patch(shell.cmd_shell, "profile_name_choices", profile_name_choices)
+ self.patch(shell.cmd_shell, "profile_name_default", profile_name_default)
parser = ArgumentParser()
subparser = shell.cmd_shell.register(parser)
@@ -66,5 +57,47 @@ def test_offers_profile_name_option_when_profiles_exist(self):
self.patch(subparser, "error").side_effect = Exception
profile_name = make_name_without_spaces("foo")
error = capture_parse_error(subparser, "--profile-name", profile_name)
- self.assertThat(str(error), Contains(
- "argument --profile-name: invalid choice: "))
+ self.assertThat(
+ str(error), Contains("argument --profile-name: invalid choice: ")
+ )
+
+ def electAttribute(self):
+ # We're going to use an attribute in this module as the means for an
+ # external script to report back, so choose an unlikely-to-be-in-use
+ # attribute.
+ module = sys.modules[__name__]
+ attrname = make_name_without_spaces("attr", sep="_")
+ self.patch(module, attrname, None)
+ return module, attrname
+
+ def callShell(self, *options):
+ parser = ArgumentParser()
+ subparser = shell.cmd_shell.register(parser)
+ options = subparser.parse_args(list(options))
+ options.execute(options)
+
+ def test_runs_script_when_specified(self):
+ module, attrname = self.electAttribute()
+
+ # Mimic a non-interactive invocation of `maas shell` with a script.
+ source = "import %s as mod; mod.%s = __file__" % (__name__, attrname)
+ script = self.makeFile("script.py", source.encode("utf-8"))
+ self.callShell(str(script))
+
+ # That attribute has been updated.
+ self.assertThat(getattr(module, attrname), Equals(str(script)))
+
+ def test_runs_stdin_when_not_interactive(self):
+ module, attrname = self.electAttribute()
+
+ # Mimic a non-interactive invocation of `maas shell`.
+ self.patch(shell, "sys")
+ shell.sys.stdin.isatty.return_value = False
+ shell.sys.stdin.read.return_value = "import %s as mod; mod.%s = __file__" % (
+ __name__,
+ attrname,
+ )
+ self.callShell()
+
+ # That attribute has been updated.
+ self.assertThat(getattr(module, attrname), Equals(""))
diff --git a/maas/client/flesh/tests/testing.py b/maas/client/flesh/tests/testing/__init__.py
similarity index 50%
rename from maas/client/flesh/tests/testing.py
rename to maas/client/flesh/tests/testing/__init__.py
index 81e1526f..2a2dfbfd 100644
--- a/maas/client/flesh/tests/testing.py
+++ b/maas/client/flesh/tests/testing/__init__.py
@@ -1,11 +1,13 @@
"""Test helpers for `maas.client.flesh`."""
-__all__ = [
- "capture_parse_error",
-]
+__all__ = ["capture_parse_error"]
import argparse
+from .... import flesh
+from ....testing import TestCase
+from ....utils.tests.test_profiles import make_profile
+
def capture_parse_error(parser, *args):
"""Capture the `ArgumentError` arising from parsing the given arguments.
@@ -20,3 +22,16 @@ def capture_parse_error(parser, *args):
return error
else:
return None
+
+
+class TestCaseWithProfile(TestCase):
+ """Base test case class for all of `flesh` commands.
+
+ This creates an empty default profile.
+ """
+
+ def setUp(self):
+ self.profile = make_profile("default")
+ self.patch(flesh, "PROFILE_NAMES", ["default"])
+ self.patch(flesh, "PROFILE_DEFAULT", self.profile)
+ super(TestCaseWithProfile, self).setUp()
diff --git a/maas/client/flesh/users.py b/maas/client/flesh/users.py
index 11d567f7..9d715908 100644
--- a/maas/client/flesh/users.py
+++ b/maas/client/flesh/users.py
@@ -1,23 +1,18 @@
"""Commands for users."""
-__all__ = [
- "register",
-]
+__all__ = ["register"]
-from . import (
- OriginTableCommand,
- tables,
-)
+from . import OriginPagedTableCommand, tables
-class cmd_list_users(OriginTableCommand):
+class cmd_users(OriginPagedTableCommand):
"""List users."""
def execute(self, origin, options, target):
table = tables.UsersTable()
- print(table.render(target, origin.Users))
+ return table.render(target, origin.Users.read())
def register(parser):
"""Register profile commands with the given parser."""
- cmd_list_users.register(parser)
+ cmd_users.register(parser)
diff --git a/maas/client/flesh/vlans.py b/maas/client/flesh/vlans.py
new file mode 100644
index 00000000..1229d38c
--- /dev/null
+++ b/maas/client/flesh/vlans.py
@@ -0,0 +1,99 @@
+"""Commands for vlans."""
+
+__all__ = ["register"]
+
+from http import HTTPStatus
+
+from . import CommandError, OriginPagedTableCommand, tables
+from ..bones import CallError
+from ..utils.maas_async import asynchronous
+
+
+class cmd_vlans(OriginPagedTableCommand):
+ """List vlans."""
+
+ def __init__(self, parser):
+ super(cmd_vlans, self).__init__(parser)
+ parser.add_argument("fabric", nargs=1, help=("Name of the fabric."))
+ parser.add_argument(
+ "--minimal", action="store_true", help=("Output only the VIDs.")
+ )
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await fabrics, await subnets
+
+ def execute(self, origin, options, target):
+ visible_columns = None
+ if options.minimal:
+ visible_columns = ("vid",)
+ try:
+ fabric = origin.Fabric.read(options.fabric[0])
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ raise CommandError("Unable to find fabric %s." % options.fabric[0])
+ else:
+ raise
+ fabrics, subnets = self.load_object_sets(origin)
+ table = tables.VlansTable(
+ visible_columns=visible_columns, fabrics=fabrics, subnets=subnets
+ )
+ return table.render(target, fabric.vlans)
+
+
+class cmd_vlan(OriginPagedTableCommand):
+ """Details of a vlan."""
+
+ def __init__(self, parser):
+ super(cmd_vlan, self).__init__(parser)
+ parser.add_argument("fabric", nargs=1, help=("Name of the fabric."))
+ parser.add_argument("vid", nargs=1, help=("VID of the VLAN."))
+
+ @asynchronous
+ async def load_object_sets(self, origin):
+ fabrics = origin.Fabrics.read()
+ subnets = origin.Subnets.read()
+ return await fabrics, await subnets
+
+ def get_vlan(self, vlans, vid):
+ for vlan in vlans:
+ if vlan.vid == vid:
+ return vlan
+ for vlan in vlans:
+ if vlan.id == vid:
+ return vlan
+
+ def execute(self, origin, options, target):
+ try:
+ fabric = origin.Fabric.read(options.fabric[0])
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ raise CommandError("Unable to find fabric %s." % options.fabric[0])
+ else:
+ raise
+ vlan_id = options.vid[0]
+ if vlan_id != "untagged":
+ try:
+ vlan_id = int(vlan_id)
+ except ValueError:
+ vlan = None
+ else:
+ vlan = self.get_vlan(fabric.vlans, options.vid[0])
+ else:
+ vlan = fabric.vlans.get_default()
+ if vlan is None:
+ raise CommandError(
+ "Unable to find VLAN %s on fabric %s."
+ % (options.vid[0], options.fabric[0])
+ )
+ fabrics, subnets = self.load_object_sets(origin)
+ table = tables.VlanDetail(fabrics=fabrics, subnets=subnets)
+ return table.render(target, vlan)
+
+
+def register(parser):
+ """Register commands with the given parser."""
+ cmd_vlans.register(parser)
+ cmd_vlan.register(parser)
diff --git a/maas/client/testing.py b/maas/client/testing.py
deleted file mode 100644
index de695027..00000000
--- a/maas/client/testing.py
+++ /dev/null
@@ -1,184 +0,0 @@
-"""Testing framework for `maas.client`."""
-
-__all__ = [
- "make_file",
- "make_mac_address",
- "make_name",
- "make_name_without_spaces",
- "make_string",
- "make_string_without_spaces",
- "pick_bool",
- "randrange",
- "TestCase",
-]
-
-import asyncio
-import doctest
-from functools import partial
-from itertools import (
- islice,
- repeat,
-)
-from os import path
-import random
-import string
-from unittest import mock
-
-from fixtures import TempDir
-import testscenarios
-from testtools import testcase
-from testtools.matchers import DocTestMatches
-
-
-random_letters = map(
- random.choice, repeat(string.ascii_letters + string.digits))
-
-random_letters_with_spaces = map(
- random.choice, repeat(string.ascii_letters + string.digits + ' '))
-
-random_octet = partial(random.randint, 0, 255)
-
-random_octets = iter(random_octet, None)
-
-
-def make_string(size=10):
- """Make a random human-readable string."""
- return "".join(islice(random_letters_with_spaces, size))
-
-
-def make_string_without_spaces(size=10):
- """Make a random human-readable string WITHOUT spaces."""
- return "".join(islice(random_letters, size))
-
-
-def make_name(prefix="name", sep='-', size=6):
- """Make a random name.
-
- :param prefix: Optional prefix. Defaults to "name".
- :param sep: Separator that will go between the prefix and the random
- portion of the name. Defaults to a dash.
- :param size: Length of the random portion of the name.
- :return: A randomized unicode string.
- """
- return prefix + sep + make_string(size)
-
-
-def make_name_without_spaces(prefix="name", sep='-', size=6):
- """Make a random name WITHOUT spaces.
-
- :param prefix: Optional prefix. Defaults to "name".
- :param sep: Separator that will go between the prefix and the random
- portion of the name. Defaults to a dash.
- :param size: Length of the random portion of the name.
- :return: A randomized unicode string.
- """
- return prefix + sep + make_string_without_spaces(size)
-
-
-def make_file(location, name=None, contents=None):
- """Create a file, and write data to it.
-
- Prefer the eponymous convenience wrapper in
- :class:`maastesting.testcase.MAASTestCase`. It creates a temporary
- directory and arranges for its eventual cleanup.
-
- :param location: Directory. Use a temporary directory for this, and
- make sure it gets cleaned up after the test!
- :param name: Optional name for the file. If none is given, one will
- be made up.
- :param contents: Optional contents for the file. If omitted, some
- arbitrary ASCII text will be written.
- :type contents: unicode, but containing only ASCII characters.
- :return: Path to the file.
- """
- if name is None:
- name = make_string()
- if contents is None:
- contents = make_string().encode('ascii')
- filename = path.join(location, name)
- with open(filename, 'wb') as f:
- f.write(contents)
- return filename
-
-
-def make_mac_address(delimiter=":"):
- """Make a MAC address string with the given delimiter."""
- octets = islice(random_octets, 6)
- return delimiter.join(format(octet, "02x") for octet in octets)
-
-
-def pick_bool():
- """Return either `True` or `False` at random."""
- return random.choice((True, False))
-
-
-def randrange(cmin=1, cmax=9):
- """Yield a random number of times between `cmin` and `cmax`."""
- return range(random.randint(cmin, cmax))
-
-
-class WithScenarios(testscenarios.WithScenarios):
- """Variant of testscenarios_' that provides ``__call__``."""
-
- def __call__(self, result=None):
- if self._get_scenarios():
- for test in testscenarios.generate_scenarios(self):
- test.__call__(result)
- else:
- super(WithScenarios, self).__call__(result)
-
-
-class TestCase(WithScenarios, testcase.TestCase):
-
- def setUp(self):
- super(TestCase, self).setUp()
- self.loop = asyncio.new_event_loop()
- self.addCleanup(self.loop.close)
- asyncio.set_event_loop(self.loop)
-
- def make_dir(self):
- """Create a temporary directory.
-
- This is a convenience wrapper around a fixture incantation. That's
- the only reason why it's on the test case and not in a factory.
- """
- return self.useFixture(TempDir()).path
-
- def make_file(self, name=None, contents=None):
- """Create, and write to, a file.
-
- This is a convenience wrapper around `make_dir` and a factory
- call. It ensures that the file is in a directory that will be
- cleaned up at the end of the test.
- """
- return make_file(self.make_dir(), name, contents)
-
- def assertDocTestMatches(self, expected, observed, flags=None):
- """See if `observed` matches `expected`, a doctest sample.
-
- By default uses the doctest flags `NORMALIZE_WHITESPACE` and
- `ELLIPSIS`.
- """
- if flags is None:
- flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
- self.assertThat(observed, DocTestMatches(expected, flags))
-
- def patch(self, obj, attribute, value=mock.sentinel.unset):
- """Patch `obj.attribute` with `value`.
-
- If `value` is unspecified, a new `MagicMock` will be created and
- patched-in instead. Its ``__name__`` attribute will be set to
- `attribute` or the ``__name__`` of the replaced object if `attribute`
- is not given.
-
- This is a thin customisation of `testtools.TestCase.patch`, so refer
- to that in case of doubt.
-
- :return: The patched-in object.
- """
- if isinstance(attribute, bytes):
- attribute = attribute.decode("ascii")
- if value is mock.sentinel.unset:
- value = mock.MagicMock(__name__=attribute)
- super(TestCase, self).patch(obj, attribute, value)
- return value
diff --git a/maas/client/testing/__init__.py b/maas/client/testing/__init__.py
new file mode 100644
index 00000000..4e740acd
--- /dev/null
+++ b/maas/client/testing/__init__.py
@@ -0,0 +1,239 @@
+"""Testing framework for `maas.client`."""
+
+__all__ = [
+ "AsyncAwaitableMock",
+ "AsyncCallableMock",
+ "AsyncContextMock",
+ "AsyncIterableMock",
+ "make_bytes",
+ "make_mac_address",
+ "make_name",
+ "make_name_without_spaces",
+ "make_range",
+ "make_string",
+ "make_string_without_spaces",
+ "pick_bool",
+ "TestCase",
+]
+
+import asyncio
+import doctest
+from functools import partial
+from itertools import islice, repeat
+from pathlib import Path
+import random
+import string
+from unittest import mock
+
+from fixtures import TempDir
+import testscenarios
+from testtools import testcase
+from testtools.matchers import DocTestMatches
+
+from ..utils.maas_async import Asynchronous
+
+
+random_letters = map(random.choice, repeat(string.ascii_letters + string.digits))
+
+random_letters_with_spaces = map(
+ random.choice, repeat(string.ascii_letters + string.digits + " ")
+)
+
+random_octet = partial(random.randint, 0, 255)
+
+random_octets = iter(random_octet, None)
+
+
+def make_string(size=10):
+ """Make a random human-readable string."""
+ return "".join(islice(random_letters_with_spaces, size))
+
+
+def make_string_without_spaces(size=10):
+ """Make a random human-readable string WITHOUT spaces."""
+ return "".join(islice(random_letters, size))
+
+
+def make_name(prefix="name", sep="-", size=6):
+ """Make a random name.
+
+ :param prefix: Optional prefix. Defaults to "name".
+ :param sep: Separator that will go between the prefix and the random
+ portion of the name. Defaults to a dash.
+ :param size: Length of the random portion of the name.
+ :return: A randomized unicode string.
+ """
+ return prefix + sep + make_string(size)
+
+
+def make_name_without_spaces(prefix="name", sep="-", size=6):
+ """Make a random name WITHOUT spaces.
+
+ :param prefix: Optional prefix. Defaults to "name".
+ :param sep: Separator that will go between the prefix and the random
+ portion of the name. Defaults to a dash.
+ :param size: Length of the random portion of the name.
+ :return: A randomized unicode string.
+ """
+ return prefix + sep + make_string_without_spaces(size)
+
+
+def make_bytes(size=10):
+ """Make a random byte string."""
+ return bytes(islice(random_octets, size))
+
+
+def make_mac_address(delimiter=":"):
+ """Make a MAC address string with the given delimiter."""
+ octets = islice(random_octets, 6)
+ return delimiter.join(format(octet, "02x") for octet in octets)
+
+
+def make_range(cmin=1, cmax=9):
+ """Return a range of random length between `cmin` and `cmax`."""
+ return range(random.randint(cmin, cmax))
+
+
+def pick_bool():
+ """Return either `True` or `False` at random."""
+ return random.choice((True, False))
+
+
+class WithScenarios(testscenarios.WithScenarios):
+ """Variant of testscenarios_' that provides ``__call__``."""
+
+ def __call__(self, result=None):
+ if self._get_scenarios():
+ for test in testscenarios.generate_scenarios(self):
+ test.__call__(result)
+ else:
+ super(WithScenarios, self).__call__(result)
+
+
+class TestCase(WithScenarios, testcase.TestCase, metaclass=Asynchronous):
+ """Base test case class for all of python-libmaas.
+
+ This creates a new `asyncio` event loop for every test, and tears it down
+ afterwards. It transparently copes with asynchronous test methods and
+ other test methods by running them in this freshly created loop.
+ """
+
+ def setUp(self):
+ super(TestCase, self).setUp()
+ self.loop = asyncio.new_event_loop()
+ self.addCleanup(self.loop.close)
+ asyncio.set_event_loop(self.loop)
+
+ def makeDir(self):
+ """Create a temporary directory.
+
+ This creates a new temporary directory. This will be removed during
+ test tear-down.
+
+ :return: The path to the directory, as a `pathlib.Path`.
+ """
+ tempdir = self.useFixture(TempDir())
+ return Path(tempdir.path)
+
+ def makeFile(self, name=None, contents=None, location=None):
+ """Create a file, and write data to it.
+
+ This creates a new file `name` in `location` and fills it with
+ `contents`. This file will be removed during test tear-down.
+
+ :param name: Name for the file; optional. If omitted, a random name
+ will be chosen.
+ :param contents: Contents for the file; optional. If omitted, some
+ arbitrary bytes will be written.
+ :param location: Path to a directory; optional. If omitted, a new
+ temporary directory will be created with `makeDir`.
+
+ :return: The path to the file, as a `pathlib.Path`.
+ """
+ location = self.makeDir() if location is None else Path(location)
+ filepath = location.joinpath(make_string() if name is None else name)
+ filepath.write_bytes(make_bytes() if contents is None else contents)
+ return filepath
+
+ def assertDocTestMatches(self, expected, observed, flags=None):
+ """See if `observed` matches `expected`, a doctest sample.
+
+ By default uses the doctest flags `NORMALIZE_WHITESPACE` and
+ `ELLIPSIS`.
+ """
+ if flags is None:
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+ self.assertThat(observed, DocTestMatches(expected, flags))
+
+ def patch(self, obj, attribute, value=mock.sentinel.unset):
+ """Patch `obj.attribute` with `value`.
+
+ If `value` is unspecified, a new `Mock` will be created and patched-in
+ instead. Its ``__name__`` attribute will be set to `attribute`.
+
+ This is a thin customisation of `testtools.TestCase.patch`, so refer
+ to that in case of doubt.
+
+ :return: The patched-in object.
+ """
+ if value is mock.sentinel.unset:
+ value = mock.Mock(__name__=attribute)
+ super(TestCase, self).patch(obj, attribute, value)
+ return value
+
+
+class AsyncAwaitableMock(mock.Mock):
+ """Mock that is "future-like"; see PEP-492.
+
+ The new `await` syntax chokes on arguments that are not future-like, i.e.
+ have an `__await__` call, so we have to fool it.
+
+ This passes calls to `__await__` through to `__call__`.
+ """
+
+ async def __await__(_mock_self, *args, **kwargs):
+ return _mock_self(*args, **kwargs)
+
+
+class AsyncCallableMock(mock.Mock):
+ """Mock which ensures calls are "future-like"; see PEP-492.
+
+ As in, calls to this mock return `return_value` or `side_effect` as usual,
+ but these are awaitable, or native coroutines.
+ """
+
+ async def __call__(_mock_self, *args, **kwargs):
+ return super().__call__(*args, **kwargs)
+
+
+class AsyncContextMock(mock.Mock):
+ """Mock that acts as an async context manager; see PEP-492.
+
+ It's not enough to mock `__aenter__` and `__aexit__` because Python
+ obtains these callable attributes from the context manager's *type*. See
+ https://www.python.org/dev/peps/pep-0492/#new-syntax. This is consistent
+ with how non-asynchronous context managers work, but it's counterintuitive
+ nonetheless.
+
+ This returns itself from `__aenter__` and `None` from `__aexit__`.
+ """
+
+ async def __aenter__(_mock_self):
+ return _mock_self
+
+ async def __aexit__(_mock_self, *exc_info):
+ return None
+
+
+class AsyncIterableMock(mock.Mock):
+ """Mock that can be asynchronously iterated; see PEP-492.
+
+ This returns itself from `__aiter__` and passes through calls to
+ `__anext__` to `__call__`.
+ """
+
+ def __aiter__(_mock_self):
+ return _mock_self
+
+ async def __anext__(_mock_self):
+ return _mock_self()
diff --git a/maas/client/tests/test_client.py b/maas/client/tests/test.py
similarity index 65%
rename from maas/client/tests/test_client.py
rename to maas/client/tests/test.py
index d37e3b2d..5b467cd2 100644
--- a/maas/client/tests/test_client.py
+++ b/maas/client/tests/test.py
@@ -1,20 +1,13 @@
"""Tests for `maas.client`."""
-__all__ = []
-
from inspect import signature
from unittest.mock import sentinel
-from testtools.matchers import (
- Equals,
- Is,
- IsInstance,
- Not,
-)
+from testtools.matchers import Equals, Is, IsInstance, Not
-from .. import _client
+from .. import facade
from ... import client
-from ..testing import TestCase
+from ..testing import AsyncCallableMock, TestCase
from ..viscera import Origin
@@ -26,13 +19,15 @@ def test__connect_matches_Origin_connect(self):
self.assertSignaturesMatch(stub, real)
def test__connect_calls_through_to_Origin(self):
- connect = self.patch(Origin, "connect")
+ connect = self.patch(Origin, "connect", AsyncCallableMock())
connect.return_value = sentinel.profile, sentinel.origin
client_object = client.connect(
- sentinel.url, apikey=sentinel.apikey, insecure=sentinel.insecure)
+ sentinel.url, apikey=sentinel.apikey, insecure=sentinel.insecure
+ )
connect.assert_called_once_with(
- sentinel.url, apikey=sentinel.apikey, insecure=sentinel.insecure)
- self.assertThat(client_object, IsInstance(_client.Client))
+ sentinel.url, apikey=sentinel.apikey, insecure=sentinel.insecure
+ )
+ self.assertThat(client_object, IsInstance(facade.Client))
self.assertThat(client_object._origin, Is(sentinel.origin))
def test__login_matches_Origin_login(self):
@@ -40,15 +35,21 @@ def test__login_matches_Origin_login(self):
self.assertSignaturesMatch(stub, real)
def test__login_calls_through_to_Origin(self):
- login = self.patch(Origin, "login")
+ login = self.patch(Origin, "login", AsyncCallableMock())
login.return_value = sentinel.profile, sentinel.origin
client_object = client.login(
- sentinel.url, username=sentinel.username,
- password=sentinel.password, insecure=sentinel.insecure)
+ sentinel.url,
+ username=sentinel.username,
+ password=sentinel.password,
+ insecure=sentinel.insecure,
+ )
login.assert_called_once_with(
- sentinel.url, username=sentinel.username,
- password=sentinel.password, insecure=sentinel.insecure)
- self.assertThat(client_object, IsInstance(_client.Client))
+ sentinel.url,
+ username=sentinel.username,
+ password=sentinel.password,
+ insecure=sentinel.insecure,
+ )
+ self.assertThat(client_object, IsInstance(facade.Client))
self.assertThat(client_object._origin, Is(sentinel.origin))
def assertSignaturesMatch(self, stub, real):
diff --git a/maas/client/tests/test__client.py b/maas/client/tests/test__client.py
deleted file mode 100644
index 3fd09509..00000000
--- a/maas/client/tests/test__client.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Tests for `maas.client._client`."""
-
-__all__ = []
-
-from unittest.mock import Mock
-
-from testtools.matchers import (
- IsInstance,
- MatchesAll,
- MatchesStructure,
-)
-
-from .. import (
- _client,
- viscera,
-)
-from ..testing import TestCase
-
-
-class TestClient(TestCase):
- """Tests for the simplified client."""
-
- def setUp(self):
- super(TestClient, self).setUp()
- self.session = Mock(name="session", handlers={})
- self.origin = viscera.Origin(self.session)
- self.client = _client.Client(self.origin)
-
- def test__client_maps_devices(self):
- self.assertThat(self.client, MatchesClient(
- devices=MatchesFacade(
- get=self.origin.Device.read,
- list=self.origin.Devices.read,
- ),
- ))
-
- def test__client_maps_machines(self):
- self.assertThat(self.client, MatchesClient(
- machines=MatchesFacade(
- allocate=self.origin.Machines.allocate,
- get=self.origin.Machine.read,
- list=self.origin.Machines.read,
- ),
- ))
-
-
-def MatchesClient(**facades):
- """Matches a `_client.Client` with the given facades."""
- return MatchesAll(
- IsInstance(_client.Client),
- MatchesStructure(**facades),
- first_only=True,
- )
-
-
-def MatchesFacade(**methods):
- """Matches a `_client.Facade` with the given methods."""
- return MatchesAll(
- IsInstance(_client.Facade),
- MatchesStructure.byEquality(**methods),
- first_only=True,
- )
diff --git a/maas/client/tests/test_facade.py b/maas/client/tests/test_facade.py
new file mode 100644
index 00000000..6e98de95
--- /dev/null
+++ b/maas/client/tests/test_facade.py
@@ -0,0 +1,282 @@
+"""Tests for `maas.client._client`."""
+
+from unittest.mock import Mock
+
+from testtools.matchers import IsInstance, MatchesAll, MatchesStructure
+
+from .. import facade, viscera
+from ..testing import TestCase
+
+
+class TestClient(TestCase):
+ """Tests for the simplified client.
+
+ Right now these are fairly trivial tests, not testing in depth. Work is in
+ progress to create a unit test framework that will allow testing against
+ fake MAAS servers that match the "shape" of real MAAS servers. At the
+ point that lands these tests should be revised.
+ """
+
+ def setUp(self):
+ super(TestClient, self).setUp()
+ self.session = Mock(name="session", handlers={})
+ self.origin = viscera.Origin(self.session)
+ self.client = facade.Client(self.origin)
+
+ def test__client_maps_account(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ account=MatchesFacade(
+ create_credentials=self.origin.Account.create_credentials,
+ delete_credentials=self.origin.Account.delete_credentials,
+ )
+ ),
+ )
+
+ def test__client_maps_boot_resources(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ boot_resources=MatchesFacade(
+ create=self.origin.BootResources.create,
+ get=self.origin.BootResource.read,
+ list=self.origin.BootResources.read,
+ start_import=self.origin.BootResources.start_import,
+ stop_import=self.origin.BootResources.stop_import,
+ )
+ ),
+ )
+
+ def test__client_maps_boot_sources(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ boot_sources=MatchesFacade(
+ create=self.origin.BootSources.create,
+ get=self.origin.BootSource.read,
+ list=self.origin.BootSources.read,
+ )
+ ),
+ )
+
+ def test__client_maps_devices(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ devices=MatchesFacade(
+ create=self.origin.Devices.create,
+ get=self.origin.Device.read,
+ list=self.origin.Devices.read,
+ )
+ ),
+ )
+
+ def test__client_maps_events(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ events=MatchesFacade(
+ query=self.origin.Events.query,
+ DEBUG=self.origin.Events.Level.DEBUG,
+ INFO=self.origin.Events.Level.INFO,
+ WARNING=self.origin.Events.Level.WARNING,
+ ERROR=self.origin.Events.Level.ERROR,
+ CRITICAL=self.origin.Events.Level.CRITICAL,
+ )
+ ),
+ )
+
+ def test__client_maps_fabrics(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ fabrics=MatchesFacade(
+ create=self.origin.Fabrics.create,
+ get=self.origin.Fabric.read,
+ get_default=self.origin.Fabric.get_default,
+ list=self.origin.Fabrics.read,
+ )
+ ),
+ )
+
+ def test__client_maps_subnets(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ subnets=MatchesFacade(
+ create=self.origin.Subnets.create,
+ get=self.origin.Subnet.read,
+ list=self.origin.Subnets.read,
+ )
+ ),
+ )
+
+ def test__client_maps_spaces(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ spaces=MatchesFacade(
+ create=self.origin.Spaces.create,
+ get=self.origin.Space.read,
+ get_default=self.origin.Space.get_default,
+ list=self.origin.Spaces.read,
+ )
+ ),
+ )
+
+ def test__client_maps_ip_ranges(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ ip_ranges=MatchesFacade(
+ create=self.origin.IPRanges.create,
+ get=self.origin.IPRange.read,
+ list=self.origin.IPRanges.read,
+ )
+ ),
+ )
+
+ def test__client_maps_static_routes(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ static_routes=MatchesFacade(
+ create=self.origin.StaticRoutes.create,
+ get=self.origin.StaticRoute.read,
+ list=self.origin.StaticRoutes.read,
+ )
+ ),
+ )
+
+ def test__client_maps_files(self):
+ self.assertThat(
+ self.client, MatchesClient(files=MatchesFacade(list=self.origin.Files.read))
+ )
+
+ def test__client_maps_machines(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ machines=MatchesFacade(
+ allocate=self.origin.Machines.allocate,
+ create=self.origin.Machines.create,
+ get=self.origin.Machine.read,
+ list=self.origin.Machines.read,
+ )
+ ),
+ )
+
+ def test__client_maps_rack_controllers(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ rack_controllers=MatchesFacade(
+ get=self.origin.RackController.read,
+ list=self.origin.RackControllers.read,
+ )
+ ),
+ )
+
+ def test__client_maps_region_controllers(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ region_controllers=MatchesFacade(
+ get=self.origin.RegionController.read,
+ list=self.origin.RegionControllers.read,
+ )
+ ),
+ )
+
+ def test__client_maps_ssh_keys(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ ssh_keys=MatchesFacade(
+ create=self.origin.SSHKeys.create,
+ get=self.origin.SSHKey.read,
+ list=self.origin.SSHKeys.read,
+ )
+ ),
+ )
+
+ def test__client_maps_tags(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ tags=MatchesFacade(
+ create=self.origin.Tags.create, list=self.origin.Tags.read
+ )
+ ),
+ )
+
+ def test__client_maps_users(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ users=MatchesFacade(
+ create=self.origin.Users.create,
+ list=self.origin.Users.read,
+ whoami=self.origin.Users.whoami,
+ )
+ ),
+ )
+
+ def test__client_maps_version(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(version=MatchesFacade(get=self.origin.Version.read)),
+ )
+
+ def test__client_maps_zones(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ zones=MatchesFacade(
+ create=self.origin.Zones.create,
+ get=self.origin.Zone.read,
+ list=self.origin.Zones.read,
+ )
+ ),
+ )
+
+ def test__client_maps_pods(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ pods=MatchesFacade(
+ create=self.origin.Pods.create,
+ get=self.origin.Pod.read,
+ list=self.origin.Pods.read,
+ )
+ ),
+ )
+
+ def test__client_maps_resource_pools(self):
+ self.assertThat(
+ self.client,
+ MatchesClient(
+ resource_pools=MatchesFacade(
+ create=self.origin.ResourcePools.create,
+ get=self.origin.ResourcePool.read,
+ list=self.origin.ResourcePools.read,
+ )
+ ),
+ )
+
+
+def MatchesClient(**facades):
+ """Matches a `facade.Client` with the given facades."""
+ return MatchesAll(
+ IsInstance(facade.Client), MatchesStructure(**facades), first_only=True
+ )
+
+
+def MatchesFacade(**methods):
+ """Matches a `facade.Facade` with the given methods."""
+ return MatchesAll(
+ IsInstance(facade.Facade),
+ MatchesStructure.byEquality(**methods),
+ first_only=True,
+ )
diff --git a/maas/client/utils/__init__.py b/maas/client/utils/__init__.py
index 9b9ac4ea..1ae5a928 100644
--- a/maas/client/utils/__init__.py
+++ b/maas/client/utils/__init__.py
@@ -1,7 +1,22 @@
-"""Utilities for the Alburnum MAAS client."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for the MAAS client."""
__all__ = [
"api_url",
+ "coalesce",
"get_all_subclasses",
"parse_docstring",
"prepare_payload",
@@ -11,40 +26,21 @@
"vars_class",
]
-from collections import Iterable
-from functools import (
- lru_cache,
- partial,
-)
-from inspect import (
- cleandoc,
- getdoc,
-)
-from itertools import (
- chain,
- cycle,
- repeat,
-)
+
+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
import re
import sys
import threading
from time import time
-from typing import Optional
-from urllib.parse import (
- ParseResult,
- quote_plus,
- urlparse,
-)
+from urllib.parse import quote_plus, urlparse
from oauthlib import oauth1
-from .async import asynchronous
-from .creds import Credentials
-from .multipart import (
- build_multipart_message,
- encode_multipart_message,
-)
-from .typecheck import typed
+from .multipart import build_multipart_message, encode_multipart_message
def urlencode(data):
@@ -56,14 +52,13 @@ def urlencode(data):
Unicode strings will be encoded to UTF-8. This is what Django expects; see
`smart_text` in the Django documentation.
"""
+
def dec(string):
if isinstance(string, bytes):
string = string.decode("utf-8")
return quote_plus(string)
- return "&".join(
- "%s=%s" % (dec(name), dec(value))
- for name, value in data)
+ return "&".join("%s=%s" % (dec(name), dec(value)) for name, value in data)
def prepare_payload(op, method, uri, data):
@@ -90,15 +85,13 @@ def slurp(opener):
if method == "GET":
headers, body = [], None
query.extend(
- (name, slurp(value) if callable(value) else value)
- for name, value in data)
+ (name, slurp(value) if callable(value) else value) for name, value in data
+ )
else:
- data = list(data)
- if len(data) == 0:
- headers, body = [], None
- else:
- message = build_multipart_message(data)
- headers, body = encode_multipart_message(message)
+ # Even if data is empty, construct a multipart request body. Piston
+ # (server-side) sets `request.data` to `None` if there's no payload.
+ message = build_multipart_message(data)
+ headers, body = encode_multipart_message(message)
uri = urlparse(uri)._replace(query=urlencode(query)).geturl()
return uri, body, headers
@@ -108,8 +101,8 @@ class OAuthSigner:
"""Helper class to OAuth-sign an HTTP request."""
def __init__(
- self, token_key, token_secret, consumer_key, consumer_secret,
- realm="OAuth"):
+ self, token_key, token_secret, consumer_key, consumer_secret, realm="OAuth"
+ ):
"""Initialize a ``OAuthAuthorizer``.
:type token_key: Unicode string.
@@ -119,6 +112,7 @@ def __init__(
:param realm: Optional.
"""
+
def _to_unicode(string):
if isinstance(string, bytes):
return string.decode("ascii")
@@ -141,9 +135,13 @@ def sign_request(self, url, method, body, headers):
# The use of PLAINTEXT here was copied from MAAS, but we should switch
# to HMAC once it works server-side.
client = oauth1.Client(
- self.consumer_key, self.consumer_secret, self.token_key,
- self.token_secret, signature_method=oauth1.SIGNATURE_PLAINTEXT,
- realm=self.realm)
+ self.consumer_key,
+ self.consumer_secret,
+ self.token_key,
+ self.token_secret,
+ signature_method=oauth1.SIGNATURE_PLAINTEXT,
+ realm=self.realm,
+ )
# To preserve API backward compatibility convert an empty string body
# to `None`. The old "oauth" library would treat the empty string as
# "no body", but "oauthlib" requires `None`.
@@ -164,18 +162,21 @@ def sign(uri, headers, credentials):
auth.sign_request(uri, method="GET", body=None, headers=headers)
-re_paragraph_splitter = re.compile(
- r"(?:\r\n){2,}|\r{2,}|\n{2,}", re.MULTILINE)
+re_paragraph_splitter = re.compile(r"(?:\r\n){2,}|\r{2,}|\n{2,}", re.MULTILINE)
paragraph_split = re_paragraph_splitter.split
docstring_split = partial(paragraph_split, maxsplit=1)
remove_line_breaks = lambda string: (
- " ".join(line.strip() for line in string.splitlines()))
+ " ".join(line.strip() for line in string.splitlines())
+)
newline = "\n"
empty = ""
+docstring = namedtuple("docstring", ("title", "body"))
+
+
@lru_cache(2**10)
def parse_docstring(thing):
"""Parse a Python docstring, or the docstring found on `thing`.
@@ -197,7 +198,7 @@ def parse_docstring(thing):
title = remove_line_breaks(title)
# Normalise line-breaks on newline.
body = body.replace("\r\n", newline).replace("\r", newline)
- return title, body
+ return docstring(title, body)
def ensure_trailing_slash(string):
@@ -234,8 +235,7 @@ def vars_class(cls):
This differs from the usual behaviour of `vars` which returns attributes
belonging to the given class and not its ancestors.
"""
- return dict(chain.from_iterable(
- vars(cls).items() for cls in reversed(cls.__mro__)))
+ return dict(chain.from_iterable(vars(cls).items() for cls in reversed(cls.__mro__)))
def retries(timeout=30, intervals=1, time=time):
@@ -294,18 +294,57 @@ def gen_retries(start, end, intervals, time=time):
break
+def coalesce(*values, default=None):
+ """Return the first argument that is not `None`.
+
+ If all arguments are `None`, return `default`, which is `None` by default.
+
+ Similar to PostgreSQL's `COALESCE` function.
+ """
+ for value in values:
+ if value is not None:
+ return value
+ else:
+ return default
+
+
+def remove_None(params: dict):
+ """Remove all keys in `params` that have the value of `None`."""
+ return {key: value for key, value in params.items() if value is not None}
+
+
+class SpinnerContext:
+ """Context of the currently running spinner."""
+
+ def __init__(self, spinner):
+ self.spinner = spinner
+ self.msg = ""
+ self._prev_msg = ""
+
+ def print(self, *args, **kwargs):
+ """Print inside of the spinner context.
+
+ This must be used when inside of a spinner context to ensure that
+ the line printed doesn't overwrite an already existing spinner line.
+ """
+ clear_len = max(len(self._prev_msg), len(self.msg)) + 4
+ self.spinner.stream.write("%s\r" % (" " * clear_len))
+ print(*args, file=self.spinner.stream, flush=True, **kwargs)
+
+
class Spinner:
"""Display a spinner at the terminal, if it's a TTY.
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
def __enter__(self):
+ self.__context = SpinnerContext(self)
if self.stream.isatty():
frames = cycle(self.frames)
stream = self.stream
@@ -319,31 +358,30 @@ def run():
# Write out successive frames (and a backspace) every 0.1
# seconds until done is set.
while not done.wait(0.1):
- stream.write("%s\b" % next(frames))
+ diff = len(self.__context._prev_msg) - len(self.__context.msg)
+ if diff < 0:
+ diff = 0
+ stream.write(
+ "[%s] %s%s\r"
+ % (next(frames), self.__context.msg, " " * diff)
+ )
+ self.__context._prev_msg = self.__context.msg
stream.flush()
finally:
- # Enable cursor.
+ # Clear line and enable cursor.
+ clear_len = (
+ max(len(self.__context._prev_msg), len(self.__context.msg)) + 4
+ )
+ stream.write("%s\r" % (" " * clear_len))
stream.write("\033[?25h")
stream.flush()
self.__done = done
self.__thread = threading.Thread(target=run)
self.__thread.start()
+ return self.__context
def __exit__(self, *exc_info):
if self.stream.isatty():
self.__done.set()
self.__thread.join()
-
-
-@typed
-@asynchronous
-async def fetch_api_description(
- url: ParseResult, credentials: Optional[Credentials],
- insecure: bool):
- """Fetch the API description from the remote MAAS instance."""
- # Circular import.
- from .. import bones
- session = await bones.SessionAPI.fromURL(
- url.geturl(), credentials=credentials, insecure=insecure)
- return session.description
diff --git a/maas/client/utils/auth.py b/maas/client/utils/auth.py
index 36d07b4d..deba7223 100644
--- a/maas/client/utils/auth.py
+++ b/maas/client/utils/auth.py
@@ -1,21 +1,23 @@
+# Copyright 2016 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
"""MAAS CLI authentication."""
-__all__ = [
- "obtain_credentials",
- "obtain_token",
- "try_getpass",
- ]
+__all__ = ["obtain_credentials", "try_getpass"]
-from getpass import (
- getpass,
- getuser,
-)
-from socket import gethostname
+from getpass import getpass
import sys
-from urllib.parse import urljoin
-
-import bs4
-import requests
from .creds import Credentials
@@ -37,68 +39,9 @@ def obtain_credentials(credentials):
if credentials == "-":
credentials = sys.stdin.readline().strip()
elif credentials is None:
- credentials = try_getpass(
- "API key (leave empty for anonymous access): ")
+ credentials = try_getpass("API key (leave empty for anonymous access): ")
# Ensure that the credentials have a valid form.
if credentials and not credentials.isspace():
return Credentials.parse(credentials)
else:
return None
-
-
-def obtain_token(url, username, password, *, insecure=False):
- """Obtain a new API key by logging into MAAS.
-
- :param url: URL for the MAAS API (i.e. ends with ``/api/x.y/``).
- :param insecure: If true, don't verify SSL/TLS certificates.
- :return: A `Credentials` instance.
- """
- url_login = urljoin(url, "../../accounts/login/")
- url_token = urljoin(url, "account/")
-
- with requests.Session() as session:
-
- # Don't verify SSL/TLS certificates by default, if requested.
- session.verify = not insecure
-
- # Fetch the log-in page.
- response = session.get(url_login)
- response.raise_for_status()
-
- # Extract the CSRF token.
- login_doc = bs4.BeautifulSoup(response.content, "html.parser")
- login_button = login_doc.find('button', text="Login")
- login_form = login_button.findParent("form")
- login_data = {
- elem["name"]: elem["value"] for elem in login_form("input")
- if elem.has_attr("name") and elem.has_attr("value")
- }
- login_data["username"] = username
- login_data["password"] = password
- # The following `requester` field is not used (at the time of
- # writing) but it ought to be associated with this new token so
- # that tokens can be selectively revoked at a later date.
- login_data["requester"] = "%s@%s" % (getuser(), gethostname())
-
- # Log-in to MAAS.
- response = session.post(url_login, login_data)
- response.raise_for_status()
-
- # Request a new API token.
- create_data = {
- "csrfmiddlewaretoken": session.cookies["csrftoken"],
- "op": "create_authorisation_token",
- }
- create_headers = {
- "Accept": "application/json",
- }
- response = session.post(url_token, create_data, create_headers)
- response.raise_for_status()
-
- # We have it!
- token = response.json()
- return Credentials(
- token["consumer_key"],
- token["token_key"],
- token["token_secret"],
- )
diff --git a/maas/client/utils/connect.py b/maas/client/utils/connect.py
deleted file mode 100644
index 4012958f..00000000
--- a/maas/client/utils/connect.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""Connect to a remote MAAS instance with an apikey."""
-
-__all__ = [
- "connect",
- "ConnectError",
-]
-
-from urllib.parse import urlparse
-
-from . import (
- api_url,
- fetch_api_description,
-)
-from .creds import Credentials
-from .profiles import Profile
-
-
-class ConnectError(Exception):
- """An error with connecting."""
-
-
-def connect(url, *, apikey=None, insecure=False):
- """Connect to a remote MAAS instance with `apikey`.
-
- Returns a new :class:`Profile` which has NOT been saved. To connect AND
- save a new profile::
-
- profile = connect(url, apikey=apikey)
- profile = profile.replace(name="mad-hatter")
-
- with profiles.ProfileStore.open() as config:
- config.save(profile)
- # Optionally, set it as the default.
- config.default = profile.name
-
- """
- url = api_url(url)
- url = urlparse(url)
-
- if url.username is not None:
- raise ConnectError(
- "Cannot provide user-name explicitly in URL (%r) when connecting; "
- "use login instead." % url.username)
- if url.password is not None:
- raise ConnectError(
- "Cannot provide password explicitly in URL (%r) when connecting; "
- "use login instead." % url.username)
-
- if apikey is None:
- credentials = None # Anonymous access.
- else:
- credentials = Credentials.parse(apikey)
-
- # Return a new (unsaved) profile.
- return Profile(
- name=url.netloc, url=url.geturl(), credentials=credentials,
- description=fetch_api_description(url, credentials, insecure))
diff --git a/maas/client/utils/creds.py b/maas/client/utils/creds.py
index be8acf59..38dc8383 100644
--- a/maas/client/utils/creds.py
+++ b/maas/client/utils/creds.py
@@ -1,3 +1,17 @@
+# Copyright 2016 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
"""Handling of MAAS API credentials.
The API client deals with credentials consisting of 3 elements: consumer key,
@@ -9,16 +23,15 @@
processes.
"""
-__all__ = [
- "Credentials",
- ]
+__all__ = ["Credentials"]
from collections import namedtuple
-from typing import Optional
+import typing
CredentialsBase = namedtuple(
- "CredentialsBase", ("consumer_key", "token_key", "token_secret"))
+ "CredentialsBase", ("consumer_key", "token_key", "token_secret")
+)
class Credentials(CredentialsBase):
@@ -27,7 +40,7 @@ class Credentials(CredentialsBase):
__slots__ = ()
@classmethod
- def parse(cls, credentials) -> Optional["Credentials"]:
+ def parse(cls, credentials) -> typing.Optional["Credentials"]:
"""Parse/interpret some given credentials.
These may take the form of:
@@ -57,7 +70,8 @@ def parse(cls, credentials) -> Optional["Credentials"]:
else:
raise ValueError(
"Malformed credentials. Expected 3 colon-separated "
- "parts, got %r." % (credentials, ))
+ "parts, got %r." % (credentials,)
+ )
else:
parts = list(credentials)
if len(parts) == 0:
@@ -67,7 +81,8 @@ def parse(cls, credentials) -> Optional["Credentials"]:
else:
raise ValueError(
"Malformed credentials. Expected 3 parts, "
- "got %r." % (credentials, ))
+ "got %r." % (credentials,)
+ )
def __str__(self):
return ":".join(self)
diff --git a/maas/client/utils/diff.py b/maas/client/utils/diff.py
new file mode 100644
index 00000000..6dde19f0
--- /dev/null
+++ b/maas/client/utils/diff.py
@@ -0,0 +1,42 @@
+# Copyright 2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helpers for calulating difference between objects."""
+
+__all__ = ["calculate_dict_diff"]
+
+
+from . import remove_None
+
+
+def calculate_dict_diff(old_params: dict, new_params: dict):
+ """Return the parameters based on the difference.
+
+ If a parameter exists in `old_params` but not in `new_params` then
+ parameter will be set to an empty string.
+ """
+ # Ignore all None values as those cannot be saved.
+ old_params = remove_None(old_params)
+ new_params = remove_None(new_params)
+ params_diff = {}
+ for key, value in old_params.items():
+ if key in new_params:
+ if value != new_params[key]:
+ params_diff[key] = new_params[key]
+ else:
+ params_diff[key] = ""
+ for key, value in new_params.items():
+ if key not in old_params:
+ params_diff[key] = value
+ return params_diff
diff --git a/maas/client/utils/login.py b/maas/client/utils/login.py
deleted file mode 100644
index 5313769f..00000000
--- a/maas/client/utils/login.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""Logging-in to a remote MAAS instance with a user-name and password.
-
-Instead of copy-and-pasting API keys, this allows clients to log-in using
-their user-name and password, and automatically retrieve an API key. These
-credentials can then be saved with the profile manager.
-"""
-
-__all__ = [
- "login",
- "LoginError",
- "PasswordWithoutUsername",
- "UsernameWithoutPassword",
-]
-
-from urllib.parse import urlparse
-
-from . import (
- api_url,
- fetch_api_description,
-)
-from .auth import obtain_token
-from .profiles import Profile
-
-
-class LoginError(Exception):
- """An error with logging-in."""
-
-
-class PasswordWithoutUsername(LoginError):
- """A password was provided without a corresponding user-name."""
-
-
-class UsernameWithoutPassword(LoginError):
- """A user-name was provided without a corresponding password."""
-
-
-def login(url, *, username=None, password=None, insecure=False):
- """Log-in to a remote MAAS instance.
-
- Returns a new :class:`Profile` which has NOT been saved. To log-in AND
- save a new profile::
-
- profile = login(url, username="alice", password="wonderland")
- profile = profile.replace(name="mad-hatter")
-
- with profiles.ProfileStore.open() as config:
- config.save(profile)
- # Optionally, set it as the default.
- config.default = profile.name
-
- """
- url = api_url(url)
- url = urlparse(url)
-
- if username is None:
- username = url.username
- else:
- if url.username is None:
- pass # Anonymous access.
- else:
- raise LoginError(
- "User-name provided explicitly (%r) and in URL (%r); "
- "provide only one." % (username, url.username))
-
- if password is None:
- password = url.password
- else:
- if url.password is None:
- pass # Anonymous access.
- else:
- raise LoginError(
- "Password provided explicitly (%r) and in URL (%r); "
- "provide only one." % (password, url.password))
-
- # Remove user-name and password from the URL.
- userinfo, _, hostinfo = url.netloc.rpartition("@")
- url = url._replace(netloc=hostinfo)
-
- if username is None:
- if password is None or len(password) == 0:
- credentials = None # Anonymous.
- else:
- raise PasswordWithoutUsername(
- "Password provided without user-name; specify user-name.")
- else:
- if password is None:
- raise UsernameWithoutPassword(
- "User-name provided without password; specify password.")
- else:
- credentials = obtain_token(
- url.geturl(), username, password, insecure=insecure)
-
- # Return a new (unsaved) profile.
- return Profile(
- name=url.netloc, url=url.geturl(), credentials=credentials,
- description=fetch_api_description(url, credentials, insecure))
diff --git a/maas/client/utils/async.py b/maas/client/utils/maas_async.py
similarity index 93%
rename from maas/client/utils/async.py
rename to maas/client/utils/maas_async.py
index 64601489..19b8bc31 100644
--- a/maas/client/utils/async.py
+++ b/maas/client/utils/maas_async.py
@@ -1,4 +1,4 @@
-# Copyright 2016 Canonical Ltd.
+# Copyright 2016-2017 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,18 +14,11 @@
"""Asynchronous helpers, for use with `asyncio`."""
-__all__ = [
- "asynchronous",
- "Asynchronous",
- "is_loop_running",
-]
+__all__ = ["asynchronous", "Asynchronous", "is_loop_running"]
from asyncio import get_event_loop
from functools import wraps
-from inspect import (
- isawaitable,
- iscoroutinefunction,
-)
+from inspect import isawaitable, iscoroutinefunction
def asynchronous(func):
@@ -40,6 +33,7 @@ def asynchronous(func):
function from outside of the event-loop, and so makes interactive use of
these APIs far more intuitive.
"""
+
@wraps(func)
def wrapper(*args, **kwargs):
eventloop = get_event_loop()
diff --git a/maas/client/utils/multipart.py b/maas/client/utils/multipart.py
index 301502b2..53f5ea54 100644
--- a/maas/client/utils/multipart.py
+++ b/maas/client/utils/multipart.py
@@ -1,20 +1,26 @@
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
"""Encoding of MIME multipart data."""
-__all__ = [
- 'encode_multipart_data',
- ]
+__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
-from io import (
- BytesIO,
- IOBase,
-)
+from io import BytesIO, IOBase
from itertools import chain
import mimetypes
@@ -48,8 +54,7 @@ def make_string_payload(name, content):
def make_file_payload(name, content):
payload = MIMEApplication(content.read())
- payload.add_header(
- "Content-Disposition", "form-data", name=name, filename=name)
+ payload.add_header("Content-Disposition", "form-data", name=name, filename=name)
names = name, getattr(content, "name", None)
payload.set_type(get_content_type(*names))
return payload
@@ -76,7 +81,16 @@ def make_payloads(name, content):
This raises `AssertionError` if it encounters anything else.
"""
- if isinstance(content, bytes):
+ if content is None:
+ yield make_bytes_payload(name, b"")
+ elif isinstance(content, bool):
+ if content:
+ yield make_bytes_payload(name, b"true")
+ else:
+ yield make_bytes_payload(name, b"false")
+ elif isinstance(content, int):
+ yield make_bytes_payload(name, b"%d" % content)
+ elif isinstance(content, bytes):
yield make_bytes_payload(name, content)
elif isinstance(content, str):
yield make_string_payload(name, content)
@@ -91,8 +105,7 @@ def make_payloads(name, content):
for payload in make_payloads(name, part):
yield payload
else:
- raise AssertionError(
- "%r is unrecognised: %r" % (name, content))
+ raise AssertionError("%r is unrecognised: %r" % (name, content))
def build_multipart_message(data):
diff --git a/maas/client/utils/profiles.py b/maas/client/utils/profiles.py
index 8abf95ab..8da4aff2 100644
--- a/maas/client/utils/profiles.py
+++ b/maas/client/utils/profiles.py
@@ -1,32 +1,31 @@
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
"""Profile configuration."""
-__all__ = [
- "Profile",
- "ProfileStore",
- "ProfileNotFound",
-]
+__all__ = ["Profile", "ProfileStore", "ProfileNotFound"]
from contextlib import contextmanager
from copy import deepcopy
import json
-import os
-from os.path import (
- exists,
- expanduser,
- isfile,
- samefile,
-)
+from pathlib import Path
import sqlite3
from textwrap import dedent
-from typing import (
- Optional,
- Sequence,
- Union,
-)
+import typing
from . import api_url
from .creds import Credentials
-from .typecheck import typed
from .types import JSONObject
@@ -35,14 +34,19 @@ class Profile(tuple):
__slots__ = ()
- @typed
def __new__(
- cls, name: str, url: str, *,
- credentials: Union[Credentials, Sequence, str, None],
- description: dict, **other: JSONObject):
- return super(Profile, cls).__new__(cls, (
- name, api_url(url), Credentials.parse(credentials),
- description, other))
+ cls,
+ name: str,
+ url: str,
+ *,
+ credentials: typing.Union[Credentials, typing.Sequence, str, None],
+ description: dict,
+ **other: JSONObject
+ ):
+ return super(Profile, cls).__new__(
+ cls,
+ (name, api_url(url), Credentials.parse(credentials), description, other),
+ )
@property
def name(self) -> str:
@@ -55,7 +59,7 @@ def url(self) -> str:
return self[1]
@property
- def credentials(self) -> Optional[Credentials]:
+ def credentials(self) -> typing.Optional[Credentials]:
"""The credentials for this profile, if set."""
return self[2]
@@ -99,25 +103,29 @@ def dump(self):
Use this value when persisting a profile.
"""
return dict(
- self.other, name=self.name, url=self.url,
- credentials=self.credentials, description=self.description,
+ self.other,
+ name=self.name,
+ url=self.url,
+ credentials=self.credentials,
+ description=self.description,
)
def __repr__(self):
if self.credentials is None:
return "<%s %s (anonymous) %s>" % (
- self.__class__.__name__, self.name, self.url)
+ self.__class__.__name__,
+ self.name,
+ self.url,
+ )
else:
- return "<%s %s %s>" % (
- self.__class__.__name__, self.name, self.url)
+ return "<%s %s %s>" % (self.__class__.__name__, self.name, self.url)
class ProfileNotFound(Exception):
"""The named profile was not found."""
def __init__(self, name):
- super(ProfileNotFound, self).__init__(
- "Profile '%s' not found." % (name,))
+ super(ProfileNotFound, self).__init__("Profile '%s' not found." % (name,))
def schema_create(conn):
@@ -128,13 +136,17 @@ def schema_create(conn):
:param conn: A connection to an SQLite3 database.
"""
- conn.execute(dedent("""\
+ conn.execute(
+ dedent(
+ """\
CREATE TABLE IF NOT EXISTS profiles
(id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
data BLOB NOT NULL,
selected BOOLEAN NOT NULL DEFAULT FALSE)
- """))
+ """
+ )
+ )
# Partial indexes are only available in >=3.8.0 and expressions in indexes
# are only available in >=3.9.0 (https://www.sqlite.org/partialindex.html
# & https://www.sqlite.org/expridx.html). Don't bother with any kind of
@@ -142,11 +154,15 @@ def schema_create(conn):
if sqlite3.sqlite_version_info >= (3, 9, 0):
# This index is for data integrity -- ensuring that only one profile
# is the default ("selected") profile -- and speed a distant second.
- conn.execute(dedent("""\
+ conn.execute(
+ dedent(
+ """\
CREATE UNIQUE INDEX IF NOT EXISTS
only_one_profile_selected ON profiles
(selected IS NOT NULL) WHERE selected
- """))
+ """
+ )
+ )
def schema_import(conn, dbpath):
@@ -160,14 +176,14 @@ def schema_import(conn, dbpath):
profiles.
:param dbpath: The filesystem path to the source SQLite3 database.
"""
- conn.execute(
- "ATTACH DATABASE ? AS source", (dbpath,))
+ conn.execute("ATTACH DATABASE ? AS source", (str(dbpath),))
conn.execute(
"INSERT OR IGNORE INTO profiles (name, data)"
" SELECT name, data FROM source.profiles"
- " WHERE data IS NOT NULL")
- conn.execute(
- "DETACH DATABASE source")
+ " WHERE data IS NOT NULL"
+ )
+ conn.commit() # need to commit before detaching the other db
+ conn.execute("DETACH DATABASE source")
class ProfileStore:
@@ -181,11 +197,10 @@ def __iter__(self):
results = self.database.execute("SELECT name FROM profiles").fetchall()
return (name for (name,) in results)
- @typed
def load(self, name: str) -> Profile:
found = self.database.execute(
- "SELECT data FROM profiles"
- " WHERE name = ?", (name,)).fetchone()
+ "SELECT data FROM profiles" " WHERE name = ?", (name,)
+ ).fetchone()
if found is None:
raise ProfileNotFound(name)
else:
@@ -193,7 +208,6 @@ def load(self, name: str) -> Profile:
state["name"] = name # Belt-n-braces.
return Profile(**state)
- @typed
def save(self, profile: Profile):
state = profile.dump()
data = json.dumps(state)
@@ -205,23 +219,22 @@ def save(self, profile: Profile):
# Ensure there's a row for this profile.
self.database.execute(
"INSERT OR IGNORE INTO profiles (name, data) VALUES (?, '')",
- (profile.name,))
+ (profile.name,),
+ )
# Update the row's data.
self.database.execute(
- "UPDATE profiles SET data = ? WHERE name = ?",
- (data, profile.name))
+ "UPDATE profiles SET data = ? WHERE name = ?", (data, profile.name)
+ )
- @typed
def delete(self, name: str):
- self.database.execute(
- "DELETE FROM profiles WHERE name = ?", (name,))
+ self.database.execute("DELETE FROM profiles WHERE name = ?", (name,))
@property
- def default(self) -> Optional[Profile]:
+ def default(self) -> typing.Optional[Profile]:
"""The name of the default profile to use, or `None`."""
found = self.database.execute(
- "SELECT name, data FROM profiles WHERE selected"
- " ORDER BY name LIMIT 1").fetchone()
+ "SELECT name, data FROM profiles WHERE selected" " ORDER BY name LIMIT 1"
+ ).fetchone()
if found is None:
return None
else:
@@ -230,13 +243,13 @@ def default(self) -> Optional[Profile]:
return Profile(**state)
@default.setter
- @typed
def default(self, profile: Profile):
with self.database:
self.save(profile)
+ del self.default
self.database.execute(
- "UPDATE profiles SET selected = (name = ?)",
- (profile.name,))
+ "UPDATE profiles SET selected = (name = ?)", (profile.name,)
+ )
@default.deleter
def default(self):
@@ -244,7 +257,11 @@ def default(self):
@classmethod
@contextmanager
- def open(cls, dbpath=expanduser("~/.maas.db")):
+ def open(
+ cls,
+ dbpath=Path("~/.maas.db").expanduser(),
+ migrate_from=Path("~/.maascli.db").expanduser(),
+ ):
"""Load a profiles database.
Called without arguments this will open (and create) a database in the
@@ -254,16 +271,19 @@ def open(cls, dbpath=expanduser("~/.maas.db")):
database on exit, saving if the exit is clean.
:param dbpath: The path to the database file to create and open.
+ :param migrate_from: Path to a database file to migrate from.
"""
+ # Ensure we're working with a Path instance.
+ dbpath = Path(dbpath)
+ migrate_from = Path(migrate_from)
# See if we ought to do a one-time migration.
- migrate_from = expanduser("~/.maascli.db")
- migrate = isfile(migrate_from) and not exists(dbpath)
+ migrate = migrate_from.is_file() and not dbpath.exists()
# Initialise filename with restrictive permissions...
- os.close(os.open(dbpath, os.O_CREAT | os.O_APPEND, 0o600))
+ dbpath.touch(mode=0o600, exist_ok=True)
# Final check to see if it's safe to migrate.
- migrate = migrate and not samefile(migrate_from, dbpath)
+ migrate = migrate and not migrate_from.samefile(dbpath)
# before opening it with sqlite.
- database = sqlite3.connect(dbpath)
+ database = sqlite3.connect(str(dbpath))
try:
store = cls(database)
if migrate:
@@ -271,7 +291,7 @@ def open(cls, dbpath=expanduser("~/.maas.db")):
yield store
else:
yield store
- except:
+ except: # noqa: E722
raise
else:
database.commit()
diff --git a/maas/client/utils/testing/__init__.py b/maas/client/utils/testing/__init__.py
new file mode 100644
index 00000000..f13beedb
--- /dev/null
+++ b/maas/client/utils/testing/__init__.py
@@ -0,0 +1,12 @@
+"""Testing helpers for `maas.client.utils`."""
+
+from ...testing import make_name_without_spaces
+from ..creds import Credentials
+
+
+def make_Credentials():
+ return Credentials(
+ make_name_without_spaces("consumer_key"),
+ make_name_without_spaces("token_key"),
+ make_name_without_spaces("secret_key"),
+ )
diff --git a/maas/client/utils/tests/test_utils.py b/maas/client/utils/tests/test.py
similarity index 54%
rename from maas/client/utils/tests/test_utils.py
rename to maas/client/utils/tests/test.py
index b9f22150..acd8e271 100644
--- a/maas/client/utils/tests/test_utils.py
+++ b/maas/client/utils/tests/test.py
@@ -1,40 +1,40 @@
-"""Tests for `maas.client.utils`."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-__all__ = []
+"""Tests for `maas.client.utils`."""
import base64
from functools import partial
from itertools import cycle
import os
import os.path
-import random
from unittest.mock import sentinel
-from urllib.parse import urlparse
from testtools.matchers import (
AfterPreprocessing,
Equals,
Is,
MatchesListwise,
+ MatchesStructure,
)
from twisted.internet.task import Clock
-from ... import (
- bones,
- utils,
-)
-from ...testing import (
- make_name,
- make_name_without_spaces,
- make_string,
- TestCase,
-)
-from ...viscera.testing import AsyncMock
-from ..creds import Credentials
+from ... import utils
+from ...testing import make_name_without_spaces, make_string, TestCase
class TestMAASOAuth(TestCase):
-
def test_OAuthSigner_sign_request_adds_header(self):
token_key = make_name_without_spaces("token-key")
token_secret = make_name_without_spaces("token-secret")
@@ -44,18 +44,23 @@ def test_OAuthSigner_sign_request_adds_header(self):
headers = {}
auth = utils.OAuthSigner(
- token_key=token_key, token_secret=token_secret,
- consumer_key=consumer_key, consumer_secret=consumer_secret,
- realm=realm)
- auth.sign_request('http://example.com/', "GET", None, headers)
+ token_key=token_key,
+ token_secret=token_secret,
+ consumer_key=consumer_key,
+ consumer_secret=consumer_secret,
+ realm=realm,
+ )
+ auth.sign_request("http://example.com/", "GET", None, headers)
- self.assertIn('Authorization', headers)
+ self.assertIn("Authorization", headers)
authorization = headers["Authorization"]
self.assertIn('realm="%s"' % realm, authorization)
self.assertIn('oauth_token="%s"' % token_key, authorization)
self.assertIn('oauth_consumer_key="%s"' % consumer_key, authorization)
- self.assertIn('oauth_signature="%s%%26%s"' % (
- consumer_secret, token_secret), authorization)
+ self.assertIn(
+ 'oauth_signature="%s%%26%s"' % (consumer_secret, token_secret),
+ authorization,
+ )
def test_sign_adds_header(self):
token_key = make_name_without_spaces("token-key")
@@ -63,10 +68,11 @@ def test_sign_adds_header(self):
consumer_key = make_name_without_spaces("consumer-key")
headers = {}
- utils.sign('http://example.com/', headers, (
- consumer_key, token_key, token_secret))
+ utils.sign(
+ "http://example.com/", headers, (consumer_key, token_key, token_secret)
+ )
- self.assertIn('Authorization', headers)
+ self.assertIn("Authorization", headers)
authorization = headers["Authorization"]
self.assertIn('realm="OAuth"', authorization)
self.assertIn('oauth_token="%s"' % token_key, authorization)
@@ -83,112 +89,194 @@ class TestPayloadPreparation(TestCase):
scenarios_without_op = (
# Without data, all requests have an empty request body and no extra
# headers.
- ("create",
- {"method": "POST", "data": [],
- "expected_uri": uri_base,
- "expected_body": None,
- "expected_headers": []}),
- ("read",
- {"method": "GET", "data": [],
- "expected_uri": uri_base,
- "expected_body": None,
- "expected_headers": []}),
- ("update",
- {"method": "PUT", "data": [],
- "expected_uri": uri_base,
- "expected_body": None,
- "expected_headers": []}),
- ("delete",
- {"method": "DELETE", "data": [],
- "expected_uri": uri_base,
- "expected_body": None,
- "expected_headers": []}),
+ (
+ "create",
+ {
+ "method": "POST",
+ "data": [],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "read",
+ {
+ "method": "GET",
+ "data": [],
+ "expected_uri": uri_base,
+ "expected_body": None,
+ "expected_headers": [],
+ },
+ ),
+ (
+ "update",
+ {
+ "method": "PUT",
+ "data": [],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "delete",
+ {
+ "method": "DELETE",
+ "data": [],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
# With data, PUT, POST, and DELETE requests have their body and
# extra headers prepared by build_multipart_message and
# encode_multipart_message. For GET requests, the data is
# encoded into the query string, and both the request body and
# extra headers are empty.
- ("create-with-data",
- {"method": "POST", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base,
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- ("read-with-data",
- {"method": "GET", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base + "?foo=bar&foo=baz",
- "expected_body": None,
- "expected_headers": []}),
- ("update-with-data",
- {"method": "PUT", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base,
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- ("delete-with-data",
- {"method": "DELETE", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base,
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- )
+ (
+ "create-with-data",
+ {
+ "method": "POST",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "read-with-data",
+ {
+ "method": "GET",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base + "?foo=bar&foo=baz",
+ "expected_body": None,
+ "expected_headers": [],
+ },
+ ),
+ (
+ "update-with-data",
+ {
+ "method": "PUT",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "delete-with-data",
+ {
+ "method": "DELETE",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base,
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ )
# Scenarios for non-ReSTful operations; i.e. with an "op" parameter.
scenarios_with_op = (
# Without data, all requests have an empty request body and no extra
# headers. The operation is encoded into the query string.
- ("create",
- {"method": "POST", "data": [],
- "expected_uri": uri_base + "?op=something",
- "expected_body": None,
- "expected_headers": []}),
- ("read",
- {"method": "GET", "data": [],
- "expected_uri": uri_base + "?op=something",
- "expected_body": None,
- "expected_headers": []}),
- ("update",
- {"method": "PUT", "data": [],
- "expected_uri": uri_base + "?op=something",
- "expected_body": None,
- "expected_headers": []}),
- ("delete",
- {"method": "DELETE", "data": [],
- "expected_uri": uri_base + "?op=something",
- "expected_body": None,
- "expected_headers": []}),
+ (
+ "create",
+ {
+ "method": "POST",
+ "data": [],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "read",
+ {
+ "method": "GET",
+ "data": [],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": None,
+ "expected_headers": [],
+ },
+ ),
+ (
+ "update",
+ {
+ "method": "PUT",
+ "data": [],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "delete",
+ {
+ "method": "DELETE",
+ "data": [],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
# With data, PUT, POST, and DELETE requests have their body and
# extra headers prepared by build_multipart_message and
# encode_multipart_message. For GET requests, the data is
# encoded into the query string, and both the request body and
# extra headers are empty. The operation is encoded into the
# query string.
- ("create-with-data",
- {"method": "POST", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base + "?op=something",
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- ("read-with-data",
- {"method": "GET", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base + "?op=something&foo=bar&foo=baz",
- "expected_body": None,
- "expected_headers": []}),
- ("update-with-data",
- {"method": "PUT", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base + "?op=something",
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- ("delete-with-data",
- {"method": "DELETE", "data": [("foo", "bar"), ("foo", "baz")],
- "expected_uri": uri_base + "?op=something",
- "expected_body": sentinel.body,
- "expected_headers": sentinel.headers}),
- )
+ (
+ "create-with-data",
+ {
+ "method": "POST",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "read-with-data",
+ {
+ "method": "GET",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base + "?op=something&foo=bar&foo=baz",
+ "expected_body": None,
+ "expected_headers": [],
+ },
+ ),
+ (
+ "update-with-data",
+ {
+ "method": "PUT",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ (
+ "delete-with-data",
+ {
+ "method": "DELETE",
+ "data": [("foo", "bar"), ("foo", "baz")],
+ "expected_uri": uri_base + "?op=something",
+ "expected_body": sentinel.body,
+ "expected_headers": sentinel.headers,
+ },
+ ),
+ )
scenarios_without_op = tuple(
("%s-without-op" % name, dict(scenario, op=None))
- for name, scenario in scenarios_without_op)
+ for name, scenario in scenarios_without_op
+ )
scenarios_with_op = tuple(
("%s-with-op" % name, dict(scenario, op="something"))
- for name, scenario in scenarios_with_op)
+ for name, scenario in scenarios_with_op
+ )
scenarios = scenarios_without_op + scenarios_with_op
@@ -202,18 +290,17 @@ def test_prepare_payload(self):
# The payload returned is a 3-tuple of (uri, body, headers). Pass
# `data` as an iterator to ensure that it works with non-sized types.
payload = utils.prepare_payload(
- op=self.op, method=self.method,
- uri=self.uri_base, data=iter(self.data))
+ op=self.op, method=self.method, uri=self.uri_base, data=iter(self.data)
+ )
expected = (
Equals(self.expected_uri),
Equals(self.expected_body),
Equals(self.expected_headers),
- )
+ )
self.assertThat(payload, MatchesListwise(expected))
# encode_multipart_message, when called, is passed the data
# unadulterated.
if self.expected_body is sentinel.body:
- build_multipart.assert_called_once_with(self.data)
encode_multipart.assert_called_once_with(sentinel.message)
@@ -223,14 +310,15 @@ class TestPayloadPreparationWithFiles(TestCase):
def test_files_are_included(self):
parameter = make_string()
contents = os.urandom(5)
- filename = self.make_file(contents=contents)
+ filepath = self.makeFile(contents=contents)
# Writing the parameter as "parameter@=filename" on the
# command-line causes name_value_pair() to return a `name,
# opener` tuple, where `opener` is a callable that returns an
# open file handle.
- data = [(parameter, partial(open, filename, "rb"))]
+ data = [(parameter, partial(filepath.open, "rb"))]
uri, body, headers = utils.prepare_payload(
- op=None, method="POST", uri="http://localhost", data=data)
+ op=None, method="POST", uri="http://localhost", data=data
+ )
expected_body_template = """\
--...
@@ -243,7 +331,10 @@ def test_files_are_included(self):
--...--
"""
expected_body = expected_body_template % (
- parameter, parameter, base64.b64encode(contents).decode("ascii"))
+ parameter,
+ parameter,
+ base64.b64encode(contents).decode("ascii"),
+ )
self.assertDocTestMatches(expected_body, body.decode("ascii"))
@@ -251,35 +342,34 @@ def test_files_are_included(self):
class TestDocstringParsing(TestCase):
"""Tests for docstring parsing with `parse_docstring`."""
- scenarios = (
- ("normal", dict(parse=utils.parse_docstring)),
- )
-
def test_basic(self):
- self.assertEqual(
- ("Title", "Body"),
- self.parse("Title\n\nBody"))
+ self.assertEqual(("Title", "Body"), utils.parse_docstring("Title\n\nBody"))
self.assertEqual(
("A longer title", "A longer body"),
- self.parse("A longer title\n\nA longer body"))
+ utils.parse_docstring("A longer title\n\nA longer body"),
+ )
+
+ def test_returns_named_tuple(self):
+ self.assertThat(
+ utils.parse_docstring("Title\n\nBody"),
+ MatchesStructure.byEquality(title="Title", body="Body"),
+ )
def test_no_body(self):
# parse_docstring returns an empty string when there's no body.
- self.assertEqual(
- ("Title", ""),
- self.parse("Title\n\n"))
- self.assertEqual(
- ("Title", ""),
- self.parse("Title"))
+ self.assertEqual(("Title", ""), utils.parse_docstring("Title\n\n"))
+ self.assertEqual(("Title", ""), utils.parse_docstring("Title"))
def test_unwrapping(self):
# parse_docstring unwraps the title paragraph, and dedents the body
# paragraphs.
self.assertEqual(
- ("Title over two lines",
- "Paragraph over\ntwo lines\n\n"
- "Another paragraph\nover two lines"),
- self.parse("""
+ (
+ "Title over two lines",
+ "Paragraph over\ntwo lines\n\n" "Another paragraph\nover two lines",
+ ),
+ utils.parse_docstring(
+ """
Title over
two lines
@@ -288,7 +378,9 @@ def test_unwrapping(self):
Another paragraph
over two lines
- """))
+ """
+ ),
+ )
def test_gets_docstring_from_function(self):
# parse_docstring can extract the docstring when the argument passed
@@ -298,55 +390,67 @@ def example():
Body.
"""
- self.assertEqual(
- ("Title.", "Body."),
- self.parse(example))
+
+ self.assertEqual(("Title.", "Body."), utils.parse_docstring(example))
def test_normalises_whitespace(self):
# parse_docstring can parse CRLF/CR/LF text, but always emits LF (\n,
# new-line) separated text.
- self.assertEqual(
- ("long title", ""),
- self.parse("long\r\ntitle"))
+ self.assertEqual(("long title", ""), utils.parse_docstring("long\r\ntitle"))
self.assertEqual(
("title", "body1\n\nbody2"),
- self.parse("title\n\nbody1\r\rbody2"))
+ utils.parse_docstring("title\n\nbody1\r\rbody2"),
+ )
class TestFunctions(TestCase):
"""Tests for miscellaneous functions in `maas.client.utils`."""
def test_api_url(self):
- transformations = list({
- "http://example.com/": "http://example.com/api/2.0/",
- "http://example.com/foo": "http://example.com/foo/api/2.0/",
- "http://example.com/foo/": "http://example.com/foo/api/2.0/",
- "http://example.com/api/7.9": "http://example.com/api/7.9/",
- "http://example.com/api/7.9/": "http://example.com/api/7.9/",
- }.items())
+ transformations = list(
+ {
+ "http://example.com/": "http://example.com/api/2.0/",
+ "http://example.com/foo": "http://example.com/foo/api/2.0/",
+ "http://example.com/foo/": "http://example.com/foo/api/2.0/",
+ "http://example.com/api/7.9": "http://example.com/api/7.9/",
+ "http://example.com/api/7.9/": "http://example.com/api/7.9/",
+ }.items()
+ )
urls = [url for url, url_out in transformations]
urls_out = [url_out for url, url_out in transformations]
expected = [
- AfterPreprocessing(utils.api_url, Equals(url_out))
- for url_out in urls_out
- ]
+ AfterPreprocessing(utils.api_url, Equals(url_out)) for url_out in urls_out
+ ]
self.assertThat(urls, MatchesListwise(expected))
+ def test_coalesce(self):
+ self.assertThat(utils.coalesce("abc"), Equals("abc"))
+ self.assertThat(utils.coalesce(None, "abc"), Equals("abc"))
+ self.assertThat(utils.coalesce("abc", None), Equals("abc"))
+ self.assertThat(utils.coalesce("abc", "def"), Equals("abc"))
+ self.assertThat(utils.coalesce(default="foo"), Equals("foo"))
+ self.assertThat(utils.coalesce(None, default="foo"), Equals("foo"))
+ self.assertThat(utils.coalesce(), Is(None))
-class TestRetries(TestCase):
+class TestRetries(TestCase):
def assertRetry(
- self, clock, observed, expected_elapsed, expected_remaining,
- expected_wait):
+ self, clock, observed, expected_elapsed, expected_remaining, expected_wait
+ ):
"""Assert that the retry tuple matches the given expectations.
Retry tuples are those returned by `retries`.
"""
- self.assertThat(observed, MatchesListwise([
- Equals(expected_elapsed), # elapsed
- Equals(expected_remaining), # remaining
- Equals(expected_wait), # wait
- ]))
+ self.assertThat(
+ observed,
+ MatchesListwise(
+ [
+ Equals(expected_elapsed), # elapsed
+ Equals(expected_remaining), # remaining
+ Equals(expected_wait), # wait
+ ]
+ ),
+ )
def test_yields_elapsed_remaining_and_wait(self):
# Take control of time.
@@ -441,21 +545,9 @@ def test_intervals_can_be_an_iterable(self):
self.assertRaises(StopIteration, next, gen_retries)
-class TestFetchAPIDescription(TestCase):
- """Tests for `fetch_api_description`."""
-
- def test__calls_through_to_SessionAPI(self):
- fromURL = self.patch(bones.SessionAPI, "fromURL", AsyncMock())
- fromURL.return_value.description = sentinel.description
+class TestRemoveNone(TestCase):
+ """Test `remove_None`."""
- url = urlparse("http://maas.example.com:5420/MAAS/")
- credentials = Credentials(
- make_name('consumer_key'), make_name('token_key'),
- make_name('secret_key'))
- insecure = random.choice((True, False))
-
- self.assertThat(
- utils.fetch_api_description(url, credentials, insecure),
- Is(sentinel.description))
- fromURL.assert_called_once_with(
- url.geturl(), credentials=credentials, insecure=insecure)
+ def test_removes_all_None_values(self):
+ data = {"None": None, "another_None": None, "keep": "value"}
+ self.assertEquals({"keep": "value"}, utils.remove_None(data))
diff --git a/maas/client/utils/tests/test_async.py b/maas/client/utils/tests/test_async.py
index b94b811f..6bf464ee 100644
--- a/maas/client/utils/tests/test_async.py
+++ b/maas/client/utils/tests/test_async.py
@@ -14,18 +14,12 @@
"""Tests for asynchronous helpers."""
-__all__ = []
-
import asyncio
from inspect import isawaitable
-from testtools.matchers import (
- Equals,
- Is,
- MatchesPredicate,
-)
+from testtools.matchers import Equals, Is, MatchesPredicate
-from .. import async
+from .. import maas_async
from ...testing import TestCase
@@ -37,30 +31,28 @@ class TestAsynchronousWrapper(TestCase):
def test_returns_plain_result_unaltered_when_loop_not_running(self):
token = object()
- func = async.asynchronous(lambda: token)
+ func = maas_async.asynchronous(lambda: token)
self.assertThat(func(), Is(token))
def test_returns_plain_result_unaltered_when_loop_running(self):
token = object()
- func = async.asynchronous(lambda: token)
+ func = maas_async.asynchronous(lambda: token)
async def within_event_loop():
loop = asyncio.get_event_loop()
self.assertTrue(loop.is_running())
return func()
- self.assertThat(
- self.loop.run_until_complete(within_event_loop()),
- Is(token))
+ self.assertThat(self.loop.run_until_complete(within_event_loop()), Is(token))
def test_blocks_on_awaitable_result_when_loop_not_running(self):
token = asyncio.sleep(0.0)
- func = async.asynchronous(lambda: token)
+ func = maas_async.asynchronous(lambda: token)
self.assertThat(func(), Is(None))
def test_returns_awaitable_result_unaltered_when_loop_running(self):
token = asyncio.sleep(0.0)
- func = async.asynchronous(lambda: token)
+ func = maas_async.asynchronous(lambda: token)
async def within_event_loop():
loop = asyncio.get_event_loop()
@@ -80,7 +72,7 @@ class TestAsynchronousType(TestCase):
def test_callable_attributes_are_wrapped(self):
# `Asynchronous` groks class- and static-methods.
- class Class(metaclass=async.Asynchronous):
+ class Class(metaclass=maas_async.Asynchronous):
attribute = 123
diff --git a/maas/client/utils/tests/test_auth.py b/maas/client/utils/tests/test_auth.py
index b2af9967..dc6b2945 100644
--- a/maas/client/utils/tests/test_auth.py
+++ b/maas/client/utils/tests/test_auth.py
@@ -1,31 +1,28 @@
-"""Tests for `maas.client.utils.auth`."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-__all__ = []
+"""Tests for `maas.client.utils.auth`."""
import sys
-from unittest.mock import (
- ANY,
- sentinel,
-)
+from unittest.mock import ANY, sentinel
from .. import auth
-from ...testing import (
- make_name,
- TestCase,
-)
-from ..creds import Credentials
-
-
-def make_credentials():
- return Credentials(
- make_name('consumer_key'),
- make_name('token_key'),
- make_name('secret_key'),
- )
+from ...testing import TestCase
+from ..testing import make_Credentials
class TestAuth(TestCase):
-
def test_try_getpass(self):
getpass = self.patch(auth, "getpass")
getpass.return_value = sentinel.credentials
@@ -41,7 +38,7 @@ def test_try_getpass_eof(self):
def test_obtain_credentials_from_stdin(self):
# When "-" is passed to obtain_credentials, it reads credentials from
# stdin, trims whitespace, and converts it into a 3-tuple of creds.
- credentials = make_credentials()
+ credentials = make_Credentials()
stdin = self.patch(sys, "stdin")
stdin.readline.return_value = str(credentials) + "\n"
self.assertEqual(credentials, auth.obtain_credentials("-"))
@@ -50,7 +47,7 @@ def test_obtain_credentials_from_stdin(self):
def test_obtain_credentials_via_getpass(self):
# When None is passed to obtain_credentials, it attempts to obtain
# credentials via getpass, then converts it into a 3-tuple of creds.
- credentials = make_credentials()
+ credentials = make_Credentials()
getpass = self.patch(auth, "getpass")
getpass.return_value = str(credentials)
self.assertEqual(credentials, auth.obtain_credentials(None))
diff --git a/maas/client/utils/tests/test_connect.py b/maas/client/utils/tests/test_connect.py
deleted file mode 100644
index 11309352..00000000
--- a/maas/client/utils/tests/test_connect.py
+++ /dev/null
@@ -1,85 +0,0 @@
-"""Tests for `maas.client.utils.connect`."""
-
-__all__ = []
-
-from urllib.parse import urlparse
-
-from testtools.matchers import (
- Equals,
- Is,
- IsInstance,
-)
-
-from .. import (
- api_url,
- connect,
- profiles,
-)
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
-from .test_auth import make_credentials
-
-
-class TestConnect(TestCase):
- """Tests for `maas.client.utils.connect.connect`."""
-
- def setUp(self):
- super(TestConnect, self).setUp()
- self.patch(connect, "fetch_api_description").return_value = {}
-
- def test__anonymous_when_no_apikey_provided(self):
- # Connect without an apikey.
- profile = connect.connect("http://example.org:5240/MAAS/")
- connect.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- None, False)
- # A Profile instance was returned with no credentials.
- self.assertThat(profile, IsInstance(profiles.Profile))
- self.assertThat(profile.credentials, Is(None))
-
- def test__connected_when_apikey_provided(self):
- credentials = make_credentials()
- # Connect with an apikey.
- profile = connect.connect(
- "http://example.org:5240/MAAS/", apikey=str(credentials))
- # The description was fetched.
- connect.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- credentials, False)
- # A Profile instance was returned with the expected credentials.
- self.assertThat(profile, IsInstance(profiles.Profile))
- self.assertThat(profile.credentials, Equals(credentials))
-
- def test__complains_when_username_in_URL(self):
- self.assertRaises(
- connect.ConnectError, connect.connect,
- "http://foo:bar@example.org:5240/MAAS/")
-
- def test__complains_when_password_in_URL(self):
- self.assertRaises(
- connect.ConnectError, connect.connect,
- "http://:bar@example.org:5240/MAAS/")
-
- def test__URL_is_normalised_to_point_at_API_endpoint(self):
- profile = connect.connect("http://example.org:5240/MAAS/")
- self.assertThat(profile.url, Equals(
- api_url("http://example.org:5240/MAAS/")))
-
- def test__profile_is_given_default_name_based_on_URL(self):
- domain = make_name_without_spaces("domain")
- profile = connect.connect("http://%s/MAAS/" % domain)
- self.assertThat(profile.name, Equals(domain))
-
- def test__API_description_is_saved_in_profile(self):
- description = connect.fetch_api_description.return_value = {
- "foo": "bar"}
- profile = connect.connect("http://example.org:5240/MAAS/")
- self.assertThat(profile.description, Equals(description))
-
- def test__API_description_is_fetched_insecurely_if_requested(self):
- connect.connect("http://example.org:5240/MAAS/", insecure=True)
- connect.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- None, True)
diff --git a/maas/client/utils/tests/test_creds.py b/maas/client/utils/tests/test_creds.py
index ab88c465..ea833016 100644
--- a/maas/client/utils/tests/test_creds.py
+++ b/maas/client/utils/tests/test_creds.py
@@ -1,6 +1,18 @@
-"""Tests for handling of MAAS API credentials."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-__all__ = []
+"""Tests for handling of MAAS API credentials."""
from testtools.matchers import IsInstance
@@ -13,7 +25,7 @@ class TestCredentials(TestCase):
def test_str_form_is_colon_separated_triple(self):
creds = Credentials("foo", "bar", "baz")
- self.assertEqual(':'.join(creds), str(creds))
+ self.assertEqual(":".join(creds), str(creds))
def test_parse_reads_a_colon_separated_triple(self):
creds = Credentials.parse("foo:bar:baz")
diff --git a/maas/client/utils/tests/test_diff.py b/maas/client/utils/tests/test_diff.py
new file mode 100644
index 00000000..8b30c01e
--- /dev/null
+++ b/maas/client/utils/tests/test_diff.py
@@ -0,0 +1,52 @@
+# Copyright 2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for handling the calculation of object difference."""
+
+import copy
+
+from ...testing import TestCase
+from ..diff import calculate_dict_diff
+
+
+class TestCalculateDictDiff(TestCase):
+ """Test `calculate_dict_diff`."""
+
+ def test_calcs_no_difference(self):
+ orig_data = {"key1": "value1", "key2": "value2"}
+ new_data = copy.deepcopy(orig_data)
+ self.assertEquals({}, calculate_dict_diff(orig_data, new_data))
+
+ def test_calcs_changed_value(self):
+ orig_data = {"key1": "value1", "key2": "value2"}
+ new_data = copy.deepcopy(orig_data)
+ new_data["key2"] = "new_value"
+ self.assertEquals(
+ {"key2": "new_value"}, calculate_dict_diff(orig_data, new_data)
+ )
+
+ def test_calcs_deleted_value(self):
+ orig_data = {"key1": "value1", "key2": "value2"}
+ new_data = copy.deepcopy(orig_data)
+ del new_data["key2"]
+ self.assertEquals({"key2": ""}, calculate_dict_diff(orig_data, new_data))
+
+ def test_calcs_changes_and_deleted(self):
+ orig_data = {"key1": "value1", "key2": "value2"}
+ new_data = copy.deepcopy(orig_data)
+ new_data["key1"] = "new_value"
+ del new_data["key2"]
+ self.assertEquals(
+ {"key1": "new_value", "key2": ""}, calculate_dict_diff(orig_data, new_data)
+ )
diff --git a/maas/client/utils/tests/test_login.py b/maas/client/utils/tests/test_login.py
deleted file mode 100644
index e99a9852..00000000
--- a/maas/client/utils/tests/test_login.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""Tests for `maas.client.utils.login`."""
-
-__all__ = []
-
-from urllib.parse import urlparse
-
-from testtools.matchers import (
- Equals,
- Is,
- IsInstance,
-)
-
-from .. import (
- api_url,
- login,
- profiles,
-)
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
-from .test_auth import make_credentials
-
-
-class TestLogin(TestCase):
- """Tests for `maas.client.utils.login.login`."""
-
- def setUp(self):
- super(TestLogin, self).setUp()
- self.patch(login, "obtain_token").return_value = None
- self.patch(login, "fetch_api_description").return_value = {}
-
- def test__anonymous_when_neither_username_nor_password_provided(self):
- # Log-in without a user-name or a password.
- profile = login.login("http://example.org:5240/MAAS/")
- # No token was obtained, but the description was fetched.
- login.obtain_token.assert_not_called()
- login.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- None, False)
- # A Profile instance was returned with no credentials.
- self.assertThat(profile, IsInstance(profiles.Profile))
- self.assertThat(profile.credentials, Is(None))
-
- def test__authenticated_when_username_and_password_provided(self):
- credentials = login.obtain_token.return_value = make_credentials()
- # Log-in with a user-name and a password.
- profile = login.login("http://foo:bar@example.org:5240/MAAS/")
- # A token was obtained, and the description was fetched.
- login.obtain_token.assert_called_once_with(
- "http://example.org:5240/MAAS/api/2.0/",
- "foo", "bar", insecure=False)
- login.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- credentials, False)
- # A Profile instance was returned with the expected credentials.
- self.assertThat(profile, IsInstance(profiles.Profile))
- self.assertThat(profile.credentials, Is(credentials))
-
- def test__complains_when_username_but_not_password(self):
- self.assertRaises(
- login.UsernameWithoutPassword, login.login,
- "http://example.org:5240/MAAS/", username="alice")
-
- def test__complains_when_password_but_not_username(self):
- self.assertRaises(
- login.PasswordWithoutUsername, login.login,
- "http://example.org:5240/MAAS/", password="wonderland")
-
- def test__complains_when_username_in_URL_and_passed_explicitly(self):
- self.assertRaises(
- login.LoginError, login.login,
- "http://foo:bar@example.org:5240/MAAS/", username="alice")
-
- def test__complains_when_empty_username_in_URL_and_passed_explicitly(self):
- self.assertRaises(
- login.LoginError, login.login,
- "http://:bar@example.org:5240/MAAS/", username="alice")
-
- def test__complains_when_password_in_URL_and_passed_explicitly(self):
- self.assertRaises(
- login.LoginError, login.login,
- "http://foo:bar@example.org:5240/MAAS/", password="wonderland")
-
- def test__complains_when_empty_password_in_URL_and_passed_explicitly(self):
- self.assertRaises(
- login.LoginError, login.login,
- "http://foo:@example.org:5240/MAAS/", password="wonderland")
-
- def test__URL_is_normalised_to_point_at_API_endpoint(self):
- profile = login.login("http://example.org:5240/MAAS/")
- self.assertThat(profile.url, Equals(
- api_url("http://example.org:5240/MAAS/")))
-
- def test__profile_is_given_default_name_based_on_URL(self):
- domain = make_name_without_spaces("domain")
- profile = login.login("http://%s/MAAS/" % domain)
- self.assertThat(profile.name, Equals(domain))
-
- def test__API_description_is_saved_in_profile(self):
- description = login.fetch_api_description.return_value = {"foo": "bar"}
- profile = login.login("http://example.org:5240/MAAS/")
- self.assertThat(profile.description, Equals(description))
-
- def test__API_token_is_fetched_insecurely_if_requested(self):
- login.login("http://foo:bar@example.org:5240/MAAS/", insecure=True)
- login.obtain_token.assert_called_once_with(
- "http://example.org:5240/MAAS/api/2.0/",
- "foo", "bar", insecure=True)
-
- def test__API_description_is_fetched_insecurely_if_requested(self):
- login.login("http://example.org:5240/MAAS/", insecure=True)
- login.fetch_api_description.assert_called_once_with(
- urlparse("http://example.org:5240/MAAS/api/2.0/"),
- None, True)
-
- def test__uses_username_from_URL_if_set(self):
- login.login("http://foo@maas.io/", password="bar")
- login.obtain_token.assert_called_once_with(
- "http://maas.io/api/2.0/", "foo", "bar", insecure=False)
-
- def test__uses_username_and_password_from_URL_if_set(self):
- login.login("http://foo:bar@maas.io/")
- login.obtain_token.assert_called_once_with(
- "http://maas.io/api/2.0/", "foo", "bar", insecure=False)
-
- def test__uses_empty_username_and_password_in_URL_if_set(self):
- login.login("http://:@maas.io/")
- login.obtain_token.assert_called_once_with(
- "http://maas.io/api/2.0/", "", "", insecure=False)
diff --git a/maas/client/utils/tests/test_multipart.py b/maas/client/utils/tests/test_multipart.py
index 6bc2006e..9aa0699b 100644
--- a/maas/client/utils/tests/test_multipart.py
+++ b/maas/client/utils/tests/test_multipart.py
@@ -1,6 +1,18 @@
-"""Test multipart MIME helpers."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-__all__ = []
+"""Test multipart MIME helpers."""
from io import BytesIO
from os import urandom
@@ -9,19 +21,10 @@
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.http.multipartparser import MultiPartParser
from django.utils.datastructures import MultiValueDict
-from testtools.matchers import (
- EndsWith,
- StartsWith,
-)
+from testtools.matchers import EndsWith, StartsWith
-from ...testing import (
- make_string,
- TestCase,
-)
-from ..multipart import (
- encode_multipart_data,
- get_content_type,
-)
+from ...testing import make_string, TestCase
+from ..multipart import encode_multipart_data, get_content_type
# Django, sigh, needs this.
settings.configure()
@@ -30,7 +33,8 @@
ahem_django_ahem = (
"If the mismatch appears to be because the parsed values "
"are base64 encoded, then check you're using a >=1.4 release "
- "of Django.")
+ "of Django."
+)
def parse_headers_and_body_with_django(headers, body):
@@ -58,51 +62,60 @@ def parse_headers_and_body_with_django(headers, body):
"CONTENT_LENGTH": headers["Content-Length"],
}
parser = MultiPartParser(
- META=meta, input_data=BytesIO(body),
- upload_handlers=[handler])
+ META=meta, input_data=BytesIO(body), upload_handlers=[handler]
+ )
return parser.parse()
class TestMultiPart(TestCase):
-
def test_get_content_type_guesses_type(self):
- guess = get_content_type('text.txt')
- self.assertEqual('text/plain', guess)
+ guess = get_content_type("text.txt")
+ self.assertEqual("text/plain", guess)
self.assertIsInstance(guess, str)
def test_encode_multipart_data_produces_bytes(self):
- data = {make_string(): make_string().encode('ascii')}
- files = {make_string(): BytesIO(make_string().encode('ascii'))}
+ data = {make_string(): make_string().encode("ascii")}
+ files = {make_string(): BytesIO(make_string().encode("ascii"))}
body, headers = encode_multipart_data(data, files)
self.assertIsInstance(body, bytes)
def test_encode_multipart_data_closes_with_closing_boundary_line(self):
- data = {'foo': make_string().encode('ascii')}
- files = {'bar': BytesIO(make_string().encode('ascii'))}
+ data = {"foo": make_string().encode("ascii")}
+ files = {"bar": BytesIO(make_string().encode("ascii"))}
body, headers = encode_multipart_data(data, files)
- self.assertThat(body, EndsWith(b'--'))
+ self.assertThat(body, EndsWith(b"--"))
def test_encode_multipart_data(self):
# The encode_multipart_data() function should take a list of
# parameters and files and encode them into a MIME
# multipart/form-data suitable for posting to the MAAS server.
- params = {"op": "add", "foo": "bar\u1234"}
+ params = {
+ "op": {"value": "add", "output": "add"},
+ "foo": {"value": "bar\u1234", "output": "bar\u1234"},
+ "none": {"value": None, "output": ""},
+ "true": {"value": True, "output": "true"},
+ "false": {"value": False, "output": "false"},
+ "int": {"value": 1, "output": "1"},
+ "bytes": {"value": b"bytes", "output": "bytes"},
+ }
random_data = urandom(32)
files = {"baz": BytesIO(random_data)}
- body, headers = encode_multipart_data(params, files)
+ body, headers = encode_multipart_data(
+ {key: value["value"] for key, value in params.items()}, files
+ )
self.assertEqual("%s" % len(body), headers["Content-Length"])
self.assertThat(
- headers["Content-Type"],
- StartsWith("multipart/form-data; boundary="))
+ headers["Content-Type"], StartsWith("multipart/form-data; boundary=")
+ )
# Round-trip through Django's multipart code.
post, files = parse_headers_and_body_with_django(headers, body)
self.assertEqual(
- {name: [value] for name, value in params.items()}, post,
- ahem_django_ahem)
+ {name: [value["output"]] for name, value in params.items()},
+ post,
+ ahem_django_ahem,
+ )
self.assertSetEqual({"baz"}, set(files))
- self.assertEqual(
- random_data, files["baz"].read(),
- ahem_django_ahem)
+ self.assertEqual(random_data, files["baz"].read(), ahem_django_ahem)
def test_encode_multipart_data_multiple_params(self):
# Sequences of parameters and files passed to
@@ -110,15 +123,11 @@ def test_encode_multipart_data_multiple_params(self):
# multiple parameters and/or files. See `make_payloads` to
# understand how it processes different types of parameter
# values.
- params_in = [
- ("one", "ABC"),
- ("one", "XYZ"),
- ("two", ["DEF", "UVW"]),
- ]
+ params_in = [("one", "ABC"), ("one", "XYZ"), ("two", ["DEF", "UVW"])]
files = [
BytesIO(b"f1"),
- open(self.make_file(contents=b"f2"), "rb"),
- open(self.make_file(contents=b"f3"), "rb"),
+ self.makeFile(contents=b"f2").open("rb"),
+ self.makeFile(contents=b"f3").open("rb"),
]
for fd in files:
self.addCleanup(fd.close)
@@ -130,32 +139,23 @@ def test_encode_multipart_data_multiple_params(self):
body, headers = encode_multipart_data(params_in, files_in)
self.assertEqual("%s" % len(body), headers["Content-Length"])
self.assertThat(
- headers["Content-Type"],
- StartsWith("multipart/form-data; boundary="))
+ headers["Content-Type"], StartsWith("multipart/form-data; boundary=")
+ )
# Round-trip through Django's multipart code.
- params_out, files_out = (
- parse_headers_and_body_with_django(headers, body))
+ params_out, files_out = parse_headers_and_body_with_django(headers, body)
params_out_expected = MultiValueDict()
params_out_expected.appendlist("one", "ABC")
params_out_expected.appendlist("one", "XYZ")
params_out_expected.appendlist("two", "DEF")
params_out_expected.appendlist("two", "UVW")
- self.assertEqual(
- params_out_expected, params_out,
- ahem_django_ahem)
+ self.assertEqual(params_out_expected, params_out, ahem_django_ahem)
files_expected = {"f-one": b"f1", "f-two": b"f2", "f-three": b"f3"}
files_observed = {name: buf.read() for name, buf in files_out.items()}
- self.assertEqual(
- files_expected, files_observed,
- ahem_django_ahem)
+ self.assertEqual(files_expected, files_observed, ahem_django_ahem)
def test_encode_multipart_data_list_params(self):
- params_in = [
- ("one", ["ABC", "XYZ"]),
- ("one", "UVW"),
- ]
+ params_in = [("one", ["ABC", "XYZ"]), ("one", "UVW")]
body, headers = encode_multipart_data(params_in, [])
- params_out, files_out = (
- parse_headers_and_body_with_django(headers, body))
- self.assertEqual({'one': ['ABC', 'XYZ', 'UVW']}, params_out)
+ params_out, files_out = parse_headers_and_body_with_django(headers, body)
+ self.assertEqual({"one": ["ABC", "XYZ", "UVW"]}, params_out)
self.assertSetEqual(set(), set(files_out))
diff --git a/maas/client/utils/tests/test_profiles.py b/maas/client/utils/tests/test_profiles.py
index 19bd9083..835dd1ee 100644
--- a/maas/client/utils/tests/test_profiles.py
+++ b/maas/client/utils/tests/test_profiles.py
@@ -1,41 +1,44 @@
-"""Tests for `maas.client.utils.profiles`."""
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-__all__ = []
+"""Tests for `maas.client.utils.profiles`."""
import contextlib
-import os
-import os.path
+from pathlib import Path
import sqlite3
-from testtools.matchers import (
- Equals,
- Is,
- Not,
-)
+from testtools.matchers import Equals, Is, Not
from twisted.python.filepath import FilePath
-from .. import profiles
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
-from ..profiles import (
- Profile,
- ProfileNotFound,
- ProfileStore,
-)
-from .test_auth import make_credentials
+from ...testing import make_name_without_spaces, TestCase
+from ..profiles import Profile, ProfileNotFound, ProfileStore
+from ..testing import make_Credentials
-def make_profile():
+def make_profile(name=None):
+ if name is None:
+ name = make_name_without_spaces("name")
return Profile(
- name=make_name_without_spaces("name"), url="http://example.com:5240/",
- credentials=make_credentials(), description={"resources": []},
- something=make_name_without_spaces("something"))
+ name=name,
+ url="http://example.com:5240/",
+ credentials=make_Credentials(),
+ description={"resources": []},
+ something=make_name_without_spaces("something"),
+ )
class TestProfile(TestCase):
-
def test__instances_are_immutable(self):
profile = make_profile()
self.assertRaises(AttributeError, setattr, profile, "name", "foo")
@@ -54,36 +57,46 @@ def test__replace_returns_a_new_profile(self):
def test__replace_returns_a_new_profile_with_modifications(self):
profile1 = make_profile()
- profile2 = profile1.replace(
- name=profile1.name + "basil", hello="world")
+ profile2 = profile1.replace(name=profile1.name + "basil", hello="world")
self.assertThat(profile2.name, Equals(profile1.name + "basil"))
- self.assertThat(profile2.other, Equals(
- dict(profile1.other, hello="world")))
+ self.assertThat(profile2.other, Equals(dict(profile1.other, hello="world")))
def test__dump_returns_dict_with_all_state(self):
profile = make_profile()
- self.assertThat(profile.dump(), Equals({
- "name": profile.name,
- "url": profile.url,
- "credentials": profile.credentials,
- "description": profile.description,
- "something": profile.other["something"],
- }))
+ self.assertThat(
+ profile.dump(),
+ Equals(
+ {
+ "name": profile.name,
+ "url": profile.url,
+ "credentials": profile.credentials,
+ "description": profile.description,
+ "something": profile.other["something"],
+ }
+ ),
+ )
def test__representation(self):
profile = make_profile()
- self.assertThat(repr(profile), Equals(
- "".format(profile)))
+ self.assertThat(
+ repr(profile), Equals("".format(profile))
+ )
def test__representation_of_anonymous_profile(self):
profile = make_profile().replace(credentials=None)
- self.assertThat(repr(profile), Equals(
- "".format(profile)))
+ self.assertThat(
+ repr(profile),
+ Equals("".format(profile)),
+ )
class TestProfileStore(TestCase):
"""Tests for `ProfileStore`."""
+ def setUp(self):
+ super(TestProfileStore, self).setUp()
+ self.null_profile = Path("/dev/null")
+
def test_init(self):
database = sqlite3.connect(":memory:")
config = ProfileStore(database)
@@ -92,8 +105,10 @@ def test_init(self):
config.database.execute(
"SELECT COUNT(*) FROM sqlite_master"
" WHERE type = 'table'"
- " AND name = 'profiles'").fetchone(),
- (1,))
+ " AND name = 'profiles'"
+ ).fetchone(),
+ (1,),
+ )
def test_profiles_pristine(self):
# A pristine configuration has no profiles.
@@ -146,59 +161,48 @@ def test_removing_profile(self):
def test_open_and_close(self):
# ProfileStore.open() returns a context manager that closes the
# database on exit.
- config_file = os.path.join(self.make_dir(), "config")
+ config_file = self.makeDir().joinpath("config")
config = ProfileStore.open(config_file)
self.assertIsInstance(config, contextlib._GeneratorContextManager)
with config as config:
self.assertIsInstance(config, ProfileStore)
- self.assertEqual(
- (1,), config.database.execute("SELECT 1").fetchone())
- self.assertRaises(
- sqlite3.ProgrammingError, config.database.execute,
- "SELECT 1")
+ self.assertEqual((1,), config.database.execute("SELECT 1").fetchone())
+ self.assertRaises(sqlite3.ProgrammingError, config.database.execute, "SELECT 1")
def test_open_permissions_new_database(self):
# ProfileStore.open() applies restrictive file permissions to newly
# created configuration databases.
- config_file = os.path.join(self.make_dir(), "config")
- with ProfileStore.open(config_file):
- perms = FilePath(config_file).getPermissions()
+ config_file = self.makeDir().joinpath("config")
+ with ProfileStore.open(config_file, self.null_profile):
+ perms = FilePath(str(config_file)).getPermissions()
self.assertEqual("rw-------", perms.shorthand())
def test_open_permissions_existing_database(self):
# ProfileStore.open() leaves the file permissions of existing
# configuration databases.
- config_file = os.path.join(self.make_dir(), "config")
- open(config_file, "wb").close() # touch.
- os.chmod(config_file, 0o644) # u=rw,go=r
- with ProfileStore.open(config_file):
- perms = FilePath(config_file).getPermissions()
+ config_file = self.makeDir().joinpath("config")
+ config_file.touch()
+ config_file.chmod(0o644) # u=rw,go=r
+ with ProfileStore.open(config_file, self.null_profile):
+ perms = FilePath(str(config_file)).getPermissions()
self.assertEqual("rw-r--r--", perms.shorthand())
def test_open_does_one_time_migration(self):
- home = self.make_dir()
- dbpath_old = os.path.join(home, ".maascli.db")
- dbpath_new = os.path.join(home, ".maas.db")
-
- def expanduser(path):
- # We expect the paths to be expanded to be one of those below.
- paths = {"~/.maas.db": dbpath_new, "~/.maascli.db": dbpath_old}
- return paths[path]
-
- # expanduser() is used by ProfileStore.open().
- self.patch(profiles, "expanduser", expanduser)
+ home = self.makeDir()
+ dbpath_old = home.joinpath(".maascli.db")
+ dbpath_new = home.joinpath(".maas.db")
# A profile that will be migrated.
profile = make_profile()
# Populate the old database with a profile. We're using the new
# ProfileStore but that's okay; the schemas are compatible.
- with ProfileStore.open(dbpath_old) as config_old:
+ with ProfileStore.open(dbpath_old, self.null_profile) as config_old:
config_old.save(profile)
# Immediately as we open the new database, profiles from the old
# database are migrated.
- with ProfileStore.open(dbpath_new) as config_new:
+ with ProfileStore.open(dbpath_new, dbpath_old) as config_new:
self.assertEqual({profile.name}, set(config_new))
profile_migrated = config_new.load(profile.name)
self.assertEqual(profile, profile_migrated)
@@ -207,11 +211,11 @@ def expanduser(path):
# After reopening the new database we see the migrated profile that we
# deleted has stayed deleted; it has not been migrated a second time.
- with ProfileStore.open(dbpath_new) as config_new:
+ with ProfileStore.open(dbpath_new, self.null_profile) as config_new:
self.assertRaises(ProfileNotFound, config_new.load, profile.name)
# It is still present and correct in the old database.
- with ProfileStore.open(dbpath_old) as config_old:
+ with ProfileStore.open(dbpath_old, self.null_profile) as config_old:
self.assertEqual(profile, config_old.load(profile.name))
@@ -236,6 +240,15 @@ def test_default_profile_is_persisted(self):
config1.default = profile
self.assertEqual(profile, config2.default)
+ def test_default_profile_switch_profile(self):
+ database = sqlite3.connect(":memory:")
+ config = ProfileStore(database)
+ profile1 = make_profile()
+ profile2 = make_profile()
+ config.default = profile1
+ config.default = profile2
+ self.assertEqual(profile2, config.default)
+
def test_default_profile_remains_default_after_subsequent_save(self):
database = sqlite3.connect(":memory:")
profile = make_profile()
diff --git a/maas/client/utils/typecheck.py b/maas/client/utils/typecheck.py
deleted file mode 100644
index 57d1c19d..00000000
--- a/maas/client/utils/typecheck.py
+++ /dev/null
@@ -1,149 +0,0 @@
-"""Check Python 3 type hints."""
-
-__all__ = [
- "ArgumentTypeError",
- "ReturnTypeError",
- "typed",
-]
-
-from functools import wraps
-import inspect
-import typing
-
-
-class AnnotationError(TypeError):
- """An annotation has not been understood."""
-
-
-class ArgumentTypeError(TypeError):
- """An argument was of the wrong type."""
-
- def __init__(self, func, name, value, expected):
- super(ArgumentTypeError, self).__init__(
- "In %s, for argument '%s', %r is not of type %s; it is of type %s."
- % (name_of(func), name, value, name_of(expected),
- name_of(type(value))))
-
-
-class ReturnTypeError(TypeError):
- """The return value was of the wrong type."""
-
- def __init__(self, func, value, expected):
- super(ReturnTypeError, self).__init__(
- "In %s, the returned value %r is not of type %s; it is of type %s."
- % (name_of(func), value, name_of(expected), name_of(type(value))))
-
-
-def typed(func):
- signature = inspect.signature(func)
- type_hints = typing.get_type_hints(func)
- types_in = tuple(get_types_in(type_hints, func))
- type_out = get_type_out(type_hints, func)
-
- def check_in(args, kwargs):
- bound = signature.bind(*args, **kwargs)
- bound.apply_defaults()
- # Check incoming arguments.
- for name, type_in in types_in:
- # An empty *args, for example, will not appear in the bound
- # arguments list, so we much check for that.
- if name in bound.arguments:
- value = bound.arguments[name]
- if not issubclass(type(value), type_in):
- raise ArgumentTypeError(func, name, value, type_in)
-
- def check_out(result):
- if not issubclass(type(result), type_out):
- raise ReturnTypeError(func, result, type_out)
-
- if inspect.iscoroutinefunction(func):
- if type_out is None:
- @wraps(func)
- async def checked(*args, **kwargs):
- check_in(args, kwargs)
- # No annotation on return value.
- return await func(*args, **kwargs)
-
- else:
- @wraps(func)
- async def checked(*args, **kwargs):
- check_in(args, kwargs)
- result = await func(*args, **kwargs)
- check_out(result)
- return result
-
- else:
- if type_out is None:
- @wraps(func)
- def checked(*args, **kwargs):
- check_in(args, kwargs)
- # No annotation on return value.
- return func(*args, **kwargs)
-
- else:
- @wraps(func)
- def checked(*args, **kwargs):
- check_in(args, kwargs)
- result = func(*args, **kwargs)
- check_out(result)
- return result
-
- return checked
-
-
-def get_types_in(hints, func):
- for name, hint in hints.items():
- if name == "return":
- pass # Not handled here.
- elif hint is None:
- yield name, type(None) # Special case for None.
- elif is_typesig(hint):
- yield name, hint
- else:
- raise AnnotationError(
- "In %s, annotation %r for argument '%s' is "
- "not understood." % (name_of(func), hint, name))
-
-
-def get_type_out(hints, func):
- if "return" in hints:
- hint = hints["return"]
- if hint is None:
- return type(None) # Special case for None.
- elif is_typesig(hint):
- return hint
- else:
- raise AnnotationError(
- "In %s, annotation %r for return value is "
- "not understood." % (name_of(func), hint))
- else:
- return None
-
-
-def is_typesig(typesig):
- if isinstance(typesig, tuple):
- if len(typesig) == 0:
- return False
- else:
- return all(map(is_typesig, typesig))
- else:
- return isinstance(typesig, type)
-
-
-def name_of(thing):
- try:
- module = thing.__module__
- except AttributeError:
- return qualname_of(thing)
- else:
- if module == "typing":
- return repr(thing)
- else:
- return qualname_of(thing)
-
-
-def qualname_of(thing):
- try:
- return thing.__qualname__
- except AttributeError:
- return repr(thing)
diff --git a/maas/client/utils/types.py b/maas/client/utils/types.py
index ee41b649..4f312006 100644
--- a/maas/client/utils/types.py
+++ b/maas/client/utils/types.py
@@ -1,21 +1,22 @@
-"""Miscellaneous types.
+# Copyright 2016-2017 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
-Bear in mind that `typecheck` cannot yet check generic types, so using types
-like `JSONObject` is currently only partly useful. However, it's still worth
-adding such annotations because they also serve a documentary purpose.
-"""
+"""Miscellaneous types."""
-__all__ = [
- "JSONArray",
- "JSONObject",
- "JSONValue",
-]
+__all__ = ["JSONArray", "JSONObject", "JSONValue"]
-from typing import (
- Dict,
- Sequence,
- Union,
-)
+from typing import Dict, Sequence, Union
#
# Types that can be represented in JSON.
@@ -24,14 +25,3 @@
JSONValue = Union[str, int, float, bool, None, "JSONArray", "JSONObject"]
JSONArray = Sequence[JSONValue]
JSONObject = Dict[str, JSONValue]
-
-# These are belt-n-braces, but they also seem to help resolve
-# forward-references reliably. Without them, things break.
-assert issubclass(str, JSONValue)
-assert issubclass(int, JSONValue)
-assert issubclass(float, JSONValue)
-assert issubclass(bool, JSONValue)
-assert issubclass(type(None), JSONValue)
-assert issubclass(list, JSONValue)
-assert issubclass(tuple, JSONValue)
-assert issubclass(dict, JSONValue)
diff --git a/maas/client/viscera/__init__.py b/maas/client/viscera/__init__.py
index b2e28331..40edd6fc 100644
--- a/maas/client/viscera/__init__.py
+++ b/maas/client/viscera/__init__.py
@@ -19,30 +19,21 @@
"OriginBase",
]
-from collections import (
- 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
from importlib import import_module
-from itertools import (
- chain,
- starmap,
-)
+from itertools import chain, starmap
from types import MethodType
-from typing import Optional
import pytz
from .. import bones
-from ..utils import (
- get_all_subclasses,
- vars_class,
-)
-from ..utils.async import Asynchronous
+from ..errors import ObjectNotLoaded
+from ..utils import get_all_subclasses, vars_class
+from ..utils.maas_async import Asynchronous
undefined = object()
@@ -60,8 +51,8 @@ def __call__(self, *args, **kwargs):
raise RuntimeError("%s has been disabled" % (self.name,))
else:
raise RuntimeError(
- "%s has been disabled; use %s instead" % (
- self.name, self.alternative))
+ "%s has been disabled; use %s instead" % (self.name, self.alternative)
+ )
def dir_class(cls):
@@ -132,7 +123,8 @@ def __init__(self, name=None):
def __get__(self, instance, owner):
if self.name is None:
- return getattr(owner._origin, owner.__name__.rstrip("s"))
+ name = owner.__name__.split(".")[0]
+ return getattr(owner._origin, name.rstrip("s"))
else:
return getattr(owner._origin, self.name)
@@ -141,7 +133,6 @@ def __set__(self, instance, value):
class ObjectType(Asynchronous, metaclass=Asynchronous):
-
def __dir__(cls):
return dir_class(cls)
@@ -149,16 +140,18 @@ def __new__(cls, name, bases, attrs):
attrs.setdefault("__slots__", ())
return super(ObjectType, cls).__new__(cls, name, bases, attrs)
- def bind(cls, origin, handler, *, name=None):
+ def bind(cls, origin, handler, handlers, *, name=None):
"""Bind this object to the given origin and handler.
:param origin: An instance of `Origin`.
:param handler: An instance of `bones.HandlerAPI`.
+ :param handlers: All handlers from `bones`.
:return: A subclass of this class.
"""
name = cls.__name__ if name is None else name
attrs = {
- "_origin": origin, "_handler": handler,
+ "_origin": origin,
+ "_handler": handler,
"__module__": "origin", # Could do better?
}
return type(name, (cls,), attrs)
@@ -175,26 +168,193 @@ def __str__(self):
return self.__class__.__qualname__
+def is_pk_descriptor(descriptor, include_alt=False):
+ """Return true if `descriptor` is a primary key."""
+ if descriptor.pk is True or type(descriptor.pk) is int:
+ return True
+ if include_alt:
+ return descriptor.alt_pk is True or type(descriptor.alt_pk) is int
+ else:
+ return False
+
+
+def get_pk_descriptors(cls):
+ """Return tuple of tuples with attribute name and descriptor on the
+ `cls` that is defined as the primary keys."""
+ pk_fields = {
+ name: descriptor
+ for name, descriptor in vars_class(cls).items()
+ if isinstance(descriptor, ObjectField) and is_pk_descriptor(descriptor)
+ }
+ alt_pk_fields = defaultdict(list)
+ for name, descriptor in vars_class(cls).items():
+ if isinstance(descriptor, ObjectField):
+ if descriptor.alt_pk is True:
+ alt_pk_fields[0].append((name, descriptor))
+ elif type(descriptor.alt_pk) is int:
+ alt_pk_fields[descriptor.alt_pk].append((name, descriptor))
+ if len(pk_fields) == 1:
+ return ((pk_fields.popitem(),), (alt_pk_fields[0],))
+ elif len(pk_fields) > 1:
+ unique_pk_fields = {
+ name: descriptor
+ for name, descriptor in pk_fields.items()
+ if descriptor.pk is True
+ }
+ if unique_pk_fields:
+ raise AttributeError(
+ "more than one field is marked as unique primary key: %s"
+ % (", ".join(sorted(pk_fields)))
+ )
+ pk_descriptors = tuple(
+ sorted(
+ ((name, descriptor) for name, descriptor in pk_fields.items()),
+ key=lambda item: item[1].pk,
+ )
+ )
+ alt_pk_descriptors = tuple(
+ alt_pk_fields[idx] for idx, (name, descriptor) in enumerate(pk_descriptors)
+ )
+ return pk_descriptors, alt_pk_descriptors
+ else:
+ return tuple(), tuple()
+
+
+def set_alt_pk_value(alt_descriptors, obj_data, data):
+ for name, descriptor in alt_descriptors:
+ if descriptor.name in data:
+ obj_data[descriptor.name] = data[descriptor.name]
+ return name
+ return None
+
+
class Object(ObjectBasics, metaclass=ObjectType):
"""An object in a MAAS installation."""
- __slots__ = "__weakref__", "_data"
+ __slots__ = ("__weakref__", "_data", "_orig_data", "_changed_data", "_loaded")
def __init__(self, data, local_data=None):
super(Object, self).__init__()
- if isinstance(data, Mapping):
- self._data = data
+ self._data = data
+ self._changed_data = {}
+ self._loaded = False
+ if isinstance(data, Mapping) and not data.get("__incomplete__", False):
+ self._reset(data)
+ self._loaded = True
else:
- raise TypeError(
- "data must be a mapping, not %s"
- % type(data).__name__)
+ descriptors, alt_descriptors = get_pk_descriptors(type(self))
+ if len(descriptors) == 1:
+ if isinstance(data, Mapping):
+ new_data = {}
+ try:
+ new_data[descriptors[0][1].name] = data[descriptors[0][1].name]
+ except KeyError:
+ found_name = set_alt_pk_value(
+ alt_descriptors[0], new_data, data
+ )
+ if found_name:
+ # Validate that the set data is correct and
+ # can be converted to the python value.
+ getattr(self, found_name)
+ else:
+ raise
+ else:
+ # Validate that the set data is correct and
+ # can be converted to the python value.
+ getattr(self, descriptors[0][0])
+ self._reset(new_data)
+ else:
+ self._reset({descriptors[0][1].name: data})
+ # Validate that the primary key is correct and
+ # can be converted to the python value.
+ getattr(self, descriptors[0][0])
+ elif len(descriptors) > 1:
+ if isinstance(data, Mapping):
+ obj_data = {}
+ found_names = []
+ for idx, (name, descriptor) in enumerate(descriptors):
+ try:
+ obj_data[descriptor.name] = data[descriptor.name]
+ except KeyError:
+ found_name = set_alt_pk_value(
+ alt_descriptors[idx], obj_data, data
+ )
+ if found_name:
+ found_names.append(found_name)
+ else:
+ raise
+ else:
+ found_names.append(name)
+ self._reset(obj_data)
+ # Validate that all set data is correct and can be
+ # converted to the python value.
+ for name in found_names:
+ getattr(self, name)
+ elif isinstance(data, Sequence):
+ obj_data = {}
+ for idx, (name, descriptor) in enumerate(descriptors):
+ obj_data[descriptor.name] = data[idx]
+ self._reset(obj_data)
+ for name, _ in descriptors:
+ # Validate that the primary key is correct and
+ # can be converted to the python value.
+ getattr(self, name)
+ else:
+ raise TypeError(
+ "data must be a mapping or a sequence, not %s"
+ % (type(data).__name__)
+ )
+ else:
+ if not isinstance(data, Mapping):
+ raise TypeError(
+ "data must be a mapping, not %s" % type(data).__name__
+ )
+ else:
+ raise ValueError(
+ "data cannot be incomplete without any primary keys " "defined"
+ )
if local_data is not None:
if isinstance(local_data, Mapping):
self._data.update(local_data)
else:
raise TypeError(
- "local_data must be a mapping, not %s"
- % type(data).__name__)
+ "local_data must be a mapping, not %s" % type(data).__name__
+ )
+
+ def _reset(self, data):
+ """Reset the object to look pristine with the given data.
+
+ All tracked changes will be discarded, and the object will be
+ ready to track new changes.
+ """
+ self._data = data
+ # Make a shallow copy of each item in the original data so that
+ # we can keep track of the changes. A shallow copy is enough,
+ # since we only care about changes that are directly related to
+ # this object.
+ self._orig_data = {key: copy(value) for key, value in data.items()}
+ self._changed_data = {}
+
+ def __getattribute__(self, name):
+ """Prevent access to fields that are not defined as primary keys on
+ the object when its not loaded."""
+ fields = {
+ name: descriptor
+ for name, descriptor in vars_class(type(self)).items()
+ if isinstance(descriptor, ObjectField)
+ }
+ if name in fields:
+ if super().__getattribute__("_loaded"):
+ return super(Object, self).__getattribute__(name)
+ elif is_pk_descriptor(fields[name], include_alt=True):
+ return super(Object, self).__getattribute__(name)
+ else:
+ raise ObjectNotLoaded(
+ "cannot access attribute '%s' of object '%s'"
+ % (name, type(self).__name__)
+ )
+ else:
+ return super(Object, self).__getattribute__(name)
def __eq__(self, other):
"""Strict equality check.
@@ -207,19 +367,150 @@ def __eq__(self, other):
def __repr__(self, *, name=None, fields=None):
if name is None:
name = self.__class__.__name__
+ unloaded = ""
+ if not self.loaded:
+ unloaded = " (unloaded)"
+ descriptors, alt_descriptors = get_pk_descriptors(type(self))
+ if descriptors:
+ fields = []
+ for idx, (field_name, _) in enumerate(descriptors):
+ if hasattr(self, field_name):
+ fields.append(field_name)
+ else:
+ for alt_field_name, _ in alt_descriptors[idx]:
+ if hasattr(self, alt_field_name):
+ fields.append(alt_field_name)
+ else:
+ fields = []
if fields is None:
fields = sorted(
- name for name, value in vars_class(type(self)).items()
- if isinstance(value, ObjectField))
+ name
+ for name, value in vars_class(type(self)).items()
+ if isinstance(value, ObjectField)
+ )
else:
fields = sorted(fields)
values = (getattr(self, name) for name in fields)
pairs = starmap("{0}={1!r}".format, zip(fields, values))
desc = " ".join(pairs)
if len(desc) == 0:
- return "<%s>" % (name, )
+ return "<%s%s>" % (name, unloaded)
+ else:
+ return "<%s %s%s>" % (name, desc, unloaded)
+
+ def __hash__(self):
+ name = str(self.__class__.__name__)
+ if hasattr(self, "id"):
+ return hash((name, self.id))
+ if hasattr(self, "system_id"):
+ return hash((name, self.system_id))
+ return None
+
+ @property
+ def loaded(self):
+ """True when the object is loaded.
+
+ Accessing any attribute (expected for the primary keys) of an unloaded
+ object will raise an `ObjectNotLoaded` exception.
+ """
+ return self._loaded
+
+ async def refresh(self):
+ """Refresh the object from MAAS."""
+ cls = type(self)
+ if hasattr(cls, "read"):
+ descriptors, alt_descriptors = get_pk_descriptors(cls)
+ if len(descriptors) == 1:
+ try:
+ obj = await cls.read(getattr(self, descriptors[0][0]))
+ except AttributeError:
+ found = False
+ for alt_name, _ in alt_descriptors[0]:
+ if hasattr(self, alt_name):
+ obj = await cls.read(getattr(self, alt_name))
+ found = True
+ break
+ if not found:
+ raise
+ elif len(descriptors) > 1:
+ args = []
+ for idx, (name, _) in enumerate(descriptors):
+ try:
+ args.append(getattr(self, name))
+ except AttributeError:
+ found = False
+ for alt_name, _ in alt_descriptors[idx]:
+ if hasattr(self, alt_name):
+ args.append(getattr(self, alt_name))
+ found = True
+ break
+ if not found:
+ raise
+ obj = await cls.read(*args)
+ else:
+ raise AttributeError(
+ "unable to perform 'refresh' no primary key " "fields defined."
+ )
+ if type(obj) is cls:
+ self._reset(obj._data)
+ self._loaded = True
+ else:
+ raise TypeError(
+ "result of '%s.read' must be '%s', not '%s'"
+ % (cls.__name__, cls.__name__, type(obj).__name__)
+ )
+ else:
+ raise AttributeError("'%s' object doesn't support refresh." % cls.__name__)
+
+ async def save(self):
+ """Save the object in MAAS."""
+ if hasattr(self._handler, "update"):
+ if self._changed_data:
+ update_data = dict(self._changed_data)
+ update_data.update(
+ {key: self._orig_data[key] for key in self._handler.params}
+ )
+ data = await self._handler.update(**update_data)
+ self._reset(data)
else:
- return "<%s %s>" % (name, desc)
+ raise AttributeError(
+ "'%s' object doesn't support save." % type(self).__name__
+ )
+
+
+def ManagedCreate(super_cls):
+ """Dynamically creates a `create` method for a `ObjectSet.Managed` class
+ that calls the `super_cls.create`.
+
+ The first positional argument that is passed to the `super_cls.create` is
+ the `_manager` that was set using `ObjectSet.Managed`. The created object
+ is added to the `ObjectSet.Managed` also placed in the correct
+ `_data[field]` and `_orig_data[field]` for the `_manager` object.
+ """
+
+ @wraps(super_cls.create)
+ async def _create(self, *args, **kwargs):
+ cls = type(self)
+ manager = getattr(cls, "_manager", None)
+ manager_field = getattr(cls, "_manager_field", None)
+ if manager is not None and manager_field is not None:
+ args = (manager,) + args
+ new_obj = await super_cls.create(*args, **kwargs)
+ self._items = self._items + [new_obj]
+ manager._data[manager_field.name] = manager._data[manager_field.name] + [
+ new_obj._data
+ ]
+ manager._orig_data[manager_field.name] = manager._orig_data[
+ manager_field.name
+ ] + [new_obj._data]
+ return new_obj
+ else:
+ raise AttributeError(
+ "create is not supported; %s is not a managed set"
+ % (super_cls.__name__)
+ )
+
+ return _create
class ObjectSet(ObjectBasics, metaclass=ObjectType):
@@ -229,20 +520,34 @@ class ObjectSet(ObjectBasics, metaclass=ObjectType):
_object = OriginObjectRef()
+ @classmethod
+ def Managed(cls, manager, field, items):
+ """Create a custom `ObjectSet` that is managed by a related `Object.`
+
+ :param manager: The manager of the `ObjectSet`. This is the `Object`
+ that manages this set of objects.
+ :param field: The field on the `manager` that created this managed
+ `ObjectSet`.
+ :param items: The items in the `ObjectSet`.
+ """
+ attrs = {"_manager": manager, "_manager_field": field}
+ if hasattr(cls, "create"):
+ attrs["create"] = ManagedCreate(cls)
+ cls = type(
+ "%s.Managed#%s" % (cls.__name__, manager.__class__.__name__), (cls,), attrs
+ )
+ return cls(items)
+
def __init__(self, items):
super(ObjectSet, self).__init__()
if isinstance(items, (Mapping, str, bytes)):
- raise TypeError(
- "data must be sequence-like, not %s"
- % type(items).__name__)
+ raise TypeError("data must be sequence-like, not %s" % type(items).__name__)
elif isinstance(items, Sequence):
self._items = items
elif isinstance(items, Iterable):
self._items = list(items)
else:
- raise TypeError(
- "data must be sequence-like, not %s"
- % type(items).__name__)
+ raise TypeError("data must be sequence-like, not %s" % type(items).__name__)
def __len__(self):
"""Return the count of items contained herein."""
@@ -292,7 +597,10 @@ def __eq__(self, other):
def __repr__(self):
return "<%s length=%d items=%r>" % (
- self.__class__.__name__, len(self._items), self._items)
+ self.__class__.__name__,
+ len(self._items),
+ self._items,
+ )
class ObjectField:
@@ -344,19 +652,25 @@ def Checked(cls, name, datum_to_value=None, value_to_datum=None, **other):
"""
attrs = {}
if datum_to_value is not None:
+
@wraps(datum_to_value)
def datum_to_value_method(instance, datum):
return datum_to_value(datum)
+
attrs["datum_to_value"] = staticmethod(datum_to_value_method)
if value_to_datum is not None:
+
@wraps(value_to_datum)
def value_to_datum_method(instance, value):
return value_to_datum(value)
+
attrs["value_to_datum"] = staticmethod(value_to_datum_method)
cls = type("%s.Checked#%s" % (cls.__name__, name), (cls,), attrs)
return cls(name, **other)
- def __init__(self, name, *, default=undefined, readonly=False):
+ def __init__(
+ self, name, *, default=undefined, readonly=False, pk=False, alt_pk=False
+ ):
"""Create a `ObjectField` with an optional default.
:param name: The name of the field. This is the name that's used to
@@ -364,11 +678,33 @@ def __init__(self, name, *, default=undefined, readonly=False):
:param default: A default value to return when `name` is not found in
the MAAS-side data dictionary.
:param readonly: If true, prevent setting or deleting of this field.
+ :param pk: If true marks the field as the unique primary key for the
+ object it is defined on. If an integer then it define its place
+ in the tuple of values that makes the object uniquely identified.
+ :param alt_pk: If true marks the field as an alternative unique
+ primary key for this object it is defined on. If an integer then
+ it define its place in the tuple of values that makes the object
+ uniquely identified.
"""
super(ObjectField, self).__init__()
self.name = name
self.default = default
- self.readonly = readonly
+ if not isinstance(readonly, bool):
+ raise TypeError("readonly must be a bool, not %s" % type(readonly).__name__)
+ else:
+ self.readonly = readonly
+ if not isinstance(pk, (bool, int)):
+ raise TypeError("pk must be a bool or an int, not %s" % type(pk).__name__)
+ else:
+ self.pk = pk
+ if self.pk is not False and alt_pk is not False:
+ raise ValueError("pk and alt_pk cannot be defined on the same field.")
+ elif not isinstance(alt_pk, (bool, int)):
+ raise TypeError(
+ "alt_pk must be a bool or an int, not %s" % (type(alt_pk).__name__)
+ )
+ else:
+ self.alt_pk = alt_pk
def datum_to_value(self, instance, datum):
"""Convert a given MAAS-side datum to a Python-side value.
@@ -414,6 +750,20 @@ def __set__(self, instance, value):
else:
datum = self.value_to_datum(instance, value)
instance._data[self.name] = datum
+ if self.name in instance._orig_data:
+ orig_datum = instance._orig_data[self.name]
+ if self.name in instance._changed_data:
+ if orig_datum == datum:
+ # Value set back to original value.
+ del instance._changed_data[self.name]
+ else:
+ # Value changed still update to new value.
+ instance._changed_data[self.name] = datum
+ elif orig_datum != datum:
+ # New value changed.
+ instance._changed_data[self.name] = datum
+ else:
+ instance._changed_data[self.name] = datum
def __delete__(self, instance):
"""Standard Python descriptor method."""
@@ -421,12 +771,186 @@ def __delete__(self, instance):
raise AttributeError("%s is read-only" % self.name)
elif self.name in instance._data:
del instance._data[self.name]
+ # Mark the field as deleted.
+ instance._changed_data[self.name] = None
else:
pass # Nothing to do.
-class ObjectMethod:
+class ObjectFieldRelated(ObjectField):
+ def __init__(
+ self,
+ name,
+ cls,
+ *,
+ default=undefined,
+ readonly=False,
+ pk=False,
+ alt_pk=False,
+ reverse=undefined,
+ use_data_setter=False,
+ map_func=None
+ ):
+ """Create a `ObjectFieldRelated` with `cls`.
+
+ :param name: The name of the field. This is the name that's used to
+ store the datum in the MAAS-side data dictionary.
+ :param cls: The name of the object class to convert into.
+ :param default: A default value to return when `name` is not found in
+ the MAAS-side data dictionary.
+ :param readonly: If true, prevent setting or deleting of this field.
+ :param pk: If true marks the field as the unique primary key for the
+ object it is defined on. If an integer then it define its place
+ in the tuple of values that makes the object uniquely identified.
+ :param alt_pk: If true marks the field as an alternative unique
+ primary key for this object it is defined on. If an integer then
+ it define its place in the tuple of values that makes the object
+ uniquely identified.
+ :param reverse: The name of the field on the returned instances of
+ `cls` to place this objects instance.
+ :param use_data_setter: When True setting the field will use the
+ `_data` from the `Object` instead of using the primary keys to
+ set on the object.
+ """
+ super(ObjectFieldRelated, self).__init__(
+ name, default=default, readonly=readonly, pk=pk, alt_pk=alt_pk
+ )
+ self.reverse = reverse
+ self.use_data_setter = use_data_setter
+ self.map_func = map_func
+ if self.map_func is None:
+ self.map_func = lambda instance, value: value
+ if not isinstance(cls, str):
+ if not issubclass(cls, Object):
+ raise TypeError("%s is not a subclass of Object" % cls)
+ else:
+ self.cls = cls.__name__
+ else:
+ self.cls = cls
+ def datum_to_value(self, instance, datum):
+ """Convert a given MAAS-side datum to a Python-side value.
+
+ :param instance: The `Object` instance on which this field is
+ currently operating. This method should treat it as read-only, for
+ example to perform validation with regards to other fields.
+ :param datum: The MAAS-side datum to validate and convert into a
+ Python-side value.
+ :return: A set of `cls` from the given datum.
+ """
+ datum = self.map_func(instance, datum)
+ if datum is None:
+ return None
+ local_data = None
+ if self.reverse is not None:
+ local_data = {}
+ if self.reverse is undefined:
+ local_data[instance.__class__.__name__.lower()] = instance
+ else:
+ local_data[self.reverse] = instance
+ # Get the class from the bound origin.
+ bound = getattr(instance._origin, self.cls)
+ return bound(datum, local_data=local_data)
+
+ def value_to_datum(self, instance, value):
+ """Convert a given Python-side value to a MAAS-side datum.
+
+ :param instance: The `Object` instance on which this field is
+ currently operating. This method should treat it as read-only, for
+ example to perform validation with regards to other fields.
+ :param datum: The Python-side value to validate and convert into a
+ MAAS-side datum.
+ :return: A datum derived from the given value.
+ """
+ if value is None:
+ return None
+ bound = getattr(instance._origin, self.cls)
+ if type(value) is bound:
+ if self.use_data_setter:
+ # Using data setter, so just return the data for the object.
+ return value._data
+ else:
+ # Use the primary keys to set the value.
+ descriptors, alt_descriptors = get_pk_descriptors(bound)
+ if len(descriptors) == 1:
+ return getattr(value, descriptors[0][0])
+ elif len(descriptors) > 1:
+ return tuple(getattr(value, name) for name, _ in descriptors)
+ else:
+ raise AttributeError(
+ "unable to perform set object no primary key "
+ "fields defined for %s" % self.cls
+ )
+ else:
+ raise TypeError("must be %s, not %s" % (self.cls, type(value).__name__))
+
+
+class ObjectFieldRelatedSet(ObjectField):
+ def __init__(
+ self, name, cls, *, reverse=undefined, default=undefined, map_func=None
+ ):
+ """Create a `ObjectFieldRelatedSet` with `cls`.
+
+ :param name: The name of the field. This is the name that's used to
+ store the datum in the MAAS-side data dictionary.
+ :param cls: The name of the object class to convert the sequence of
+ data into.
+ :param reverse: The name of the field on the returned instances of
+ `cls` to place this objects instance.
+ :param default: A default value to return when `name` is not found in
+ the MAAS-side data dictionary.
+ """
+ if default is undefined:
+ default = []
+ super(ObjectFieldRelatedSet, self).__init__(
+ name, default=default, readonly=True
+ )
+ self.reverse = reverse
+ self.map_func = map_func
+ if self.map_func is None:
+ self.map_func = lambda instance, value: value
+ if not isinstance(cls, str):
+ if not issubclass(cls, ObjectSet):
+ raise TypeError("%s is not a subclass of ObjectSet" % cls)
+ else:
+ self.cls = cls.__name__
+ else:
+ self.cls = cls
+
+ def datum_to_value(self, instance, datum):
+ """Convert a given MAAS-side datum to a Python-side value.
+
+ :param instance: The `Object` instance on which this field is
+ currently operating. This method should treat it as read-only, for
+ example to perform validation with regards to other fields.
+ :param datum: The MAAS-side datum to validate and convert into a
+ Python-side value.
+ :return: A set of `cls` from the given datum.
+ """
+ if datum is None:
+ return []
+ if not isinstance(datum, Sequence):
+ raise TypeError("datum must be a sequence, not %s" % type(datum).__name__)
+ local_data = None
+ if self.reverse is not None:
+ local_data = {}
+ if self.reverse is undefined:
+ local_data[instance.__class__.__name__.lower()] = instance
+ else:
+ local_data[self.reverse] = instance
+ # Get the class from the bound origin.
+ bound = getattr(instance._origin, self.cls)
+ return bound.Managed(
+ instance,
+ self,
+ (
+ bound._object(self.map_func(instance, item), local_data=local_data)
+ for item in datum
+ ),
+ )
+
+
+class ObjectMethod:
def __init__(self, _classmethod=None, _instancemethod=None):
super(ObjectMethod, self).__init__()
self.__classmethod = _classmethod
@@ -435,14 +959,14 @@ def __init__(self, _classmethod=None, _instancemethod=None):
def __get__(self, instance, owner):
if instance is None:
if self.__classmethod is None:
- raise AttributeError(
- "%s has no matching class attribute" % (instance, ))
+ raise AttributeError("%s has no matching class attribute" % (instance,))
else:
return MethodType(self.__classmethod, owner)
else:
if self.__instancemethod is None:
raise AttributeError(
- "%s has no matching instance attribute" % (instance, ))
+ "%s has no matching instance attribute" % (instance,)
+ )
else:
return MethodType(self.__instancemethod, instance)
@@ -450,8 +974,7 @@ def __set__(self, instance, value):
# Non-data descriptors (those without __set__) can be shadowed by
# instance attributes, so prevent that by making this a read-only data
# descriptor.
- raise AttributeError(
- "%s has no matching instance attribute" % (instance, ))
+ raise AttributeError("%s has no matching instance attribute" % (instance,))
def classmethod(self, func):
"""Set/modify the class method."""
@@ -500,7 +1023,7 @@ def __populate(self):
handler = handlers.get(name, None)
base = self.__objects.get(name, Object)
assert issubclass(type(base), ObjectType)
- obj = base.bind(self, handler, name=name)
+ obj = base.bind(self, handler, handlers, name=name)
# Those objects without a custom class are "hidden" by prefixing
# their name with an underscore.
objname = "_%s" % name if base is Object else name
@@ -512,18 +1035,25 @@ def __populate(self):
#
+def to(cls):
+ def to_cls(value):
+ return cls(value)
+
+ return to_cls
+
+
def check(expected):
def checker(value):
if issubclass(type(value), expected):
return value
else:
- raise TypeError(
- "%r is not of type %s" % (value, expected))
+ raise TypeError("%r is not of type %s" % (value, expected))
+
return checker
def check_optional(expected):
- return check(Optional[expected])
+ return check((expected, type(None)))
def parse_timestamp(created):
@@ -533,15 +1063,12 @@ def parse_timestamp(created):
def mapping_of(cls):
"""Expects a mapping from some key to data for `cls` instances."""
+
def mapper(data):
if not isinstance(data, Mapping):
- raise TypeError(
- "data must be a mapping, not %s"
- % type(data).__name__)
- return {
- key: cls(value)
- for key, value in data.items()
- }
+ raise TypeError("data must be a mapping, not %s" % type(data).__name__)
+ return {key: cls(value) for key, value in data.items()}
+
return mapper
@@ -559,21 +1086,19 @@ def find_objects(modules):
"""
return {
subclass.__name__: subclass
- for subclass in chain(
- get_all_subclasses(Object),
- get_all_subclasses(ObjectSet),
- )
+ for subclass in chain(get_all_subclasses(Object), get_all_subclasses(ObjectSet))
if subclass.__module__ in modules
}
-class OriginType(type):
+class OriginType(Asynchronous):
"""Metaclass for `Origin`."""
- def fromURL(cls, url, *, credentials=None, insecure=False):
+ async def fromURL(cls, url, *, credentials=None, insecure=False):
"""Return an `Origin` for a given MAAS instance."""
- session = bones.SessionAPI.fromURL(
- url, credentials=credentials, insecure=insecure)
+ session = await bones.SessionAPI.fromURL(
+ url, credentials=credentials, insecure=insecure
+ )
return cls(session)
def fromProfile(cls, profile):
@@ -592,28 +1117,28 @@ def fromProfileName(cls, name):
session = bones.SessionAPI.fromProfileName(name)
return cls(session)
- def login(
- cls, url, *, username=None, password=None, insecure=False):
+ async def login(cls, url, *, username=None, password=None, insecure=False):
"""Make an `Origin` by logging-in with a username and password.
:return: A tuple of ``profile`` and ``origin``, where the former is an
unsaved `Profile` instance, and the latter is an `Origin` instance
made using the profile.
"""
- profile, session = bones.SessionAPI.login(
- url=url, username=username, password=password, insecure=insecure)
+ profile, session = await bones.SessionAPI.login(
+ url=url, username=username, password=password, insecure=insecure
+ )
return profile, cls(session)
- def connect(
- cls, url, *, apikey=None, insecure=False):
+ async def connect(cls, url, *, apikey=None, insecure=False):
"""Make an `Origin` by connecting with an apikey.
:return: A tuple of ``profile`` and ``origin``, where the former is an
unsaved `Profile` instance, and the latter is an `Origin` instance
made using the profile.
"""
- profile, session = bones.SessionAPI.connect(
- url=url, apikey=apikey, insecure=insecure)
+ profile, session = await bones.SessionAPI.connect(
+ url=url, apikey=apikey, insecure=insecure
+ )
return profile, cls(session)
def __dir__(cls):
@@ -635,23 +1160,49 @@ def __init__(self, session):
modules = {
".",
".account",
+ ".bcache_cache_sets",
+ ".bcaches",
+ ".block_devices",
".boot_resources",
- ".boot_sources",
".boot_source_selections",
+ ".boot_sources",
".controllers",
".devices",
+ ".dnsresources",
+ ".dnsresourcerecords",
+ ".domains",
".events",
+ ".subnets",
+ ".fabrics",
+ ".spaces",
".files",
+ ".filesystem_groups",
+ ".filesystems",
+ ".interfaces",
+ ".ipranges",
+ ".ip_addresses",
+ ".logical_volumes",
".maas",
".machines",
+ ".nodes",
+ ".partitions",
+ ".pods",
+ ".raids",
+ ".resource_pools",
+ ".spaces",
+ ".sshkeys",
+ ".static_routes",
+ ".subnets",
".tags",
".users",
".version",
+ ".vlans",
+ ".volume_groups",
".zones",
}
super(Origin, self).__init__(
- session, objects=find_objects({
- import_module(name, __name__).__name__
- for name in modules
- }),
+ session,
+ objects=find_objects(
+ {import_module(name, __name__).__name__ for name in modules}
+ ),
)
diff --git a/maas/client/viscera/account.py b/maas/client/viscera/account.py
index 2c63a2a1..13ffbe83 100644
--- a/maas/client/viscera/account.py
+++ b/maas/client/viscera/account.py
@@ -1,29 +1,24 @@
"""Objects for accounts."""
-__all__ = [
- "Account",
-]
-
-from . import (
- Object,
- ObjectType,
-)
+__all__ = ["Account"]
+
+from . import Object, ObjectType
from ..utils.creds import Credentials
-from ..utils.typecheck import typed
class AccountType(ObjectType):
"""Metaclass for `Account`."""
- @typed
async def create_credentials(cls) -> Credentials:
data = await cls._handler.create_authorisation_token()
- return Credentials(**data)
+ return Credentials(
+ consumer_key=data["consumer_key"],
+ token_key=data["token_key"],
+ token_secret=data["token_secret"],
+ )
- @typed
async def delete_credentials(cls, credentials: Credentials) -> None:
- await cls._handler.delete_authorisation_token(
- token_key=credentials.token_key)
+ await cls._handler.delete_authorisation_token(token_key=credentials.token_key)
class Account(Object, metaclass=AccountType):
diff --git a/maas/client/viscera/bcache_cache_sets.py b/maas/client/viscera/bcache_cache_sets.py
new file mode 100644
index 00000000..b505bd1b
--- /dev/null
+++ b/maas/client/viscera/bcache_cache_sets.py
@@ -0,0 +1,92 @@
+"""Objects for cache sets."""
+
+__all__ = ["BcacheCacheSet", "BcacheCacheSets"]
+
+from typing import Union
+
+from . import ObjectSet, ObjectType
+from .nodes import Node
+from .block_devices import BlockDevice
+from .partitions import Partition
+from .filesystem_groups import DeviceField, FilesystemGroup
+
+
+class BcacheCacheSetType(ObjectType):
+ """Metaclass for `BcacheCacheSet`."""
+
+ async def read(cls, node, id):
+ """Get `BcacheCacheSet` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ return cls(await cls._handler.read(system_id=system_id, id=id))
+
+
+class BcacheCacheSet(FilesystemGroup, metaclass=BcacheCacheSetType):
+ """A cache set on a machine."""
+
+ cache_device = DeviceField("cache_device")
+
+ def __repr__(self):
+ return super(BcacheCacheSet, self).__repr__(fields={"name", "cache_device"})
+
+ async def delete(self):
+ """Delete this cache set."""
+ await self._handler.delete(system_id=self.node.system_id, id=self.id)
+
+
+class BcacheCacheSetsType(ObjectType):
+ """Metaclass for `BcacheCacheSets`."""
+
+ async def read(cls, node):
+ """Get list of `BcacheCacheSet`'s for `node`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ data = await cls._handler.read(system_id=system_id)
+ return cls(
+ cls._object(item, local_data={"node_system_id": system_id}) for item in data
+ )
+
+ async def create(
+ cls, node: Union[Node, str], cache_device: Union[BlockDevice, Partition]
+ ):
+ """
+ Create a BcacheCacheSet on a Node.
+
+ :param node: Node to create the interface on.
+ :type node: `Node` or `str`
+ :param cache_device: Block device or partition to create
+ the cache set on.
+ :type cache_device: `BlockDevice` or `Partition`
+ """
+ params = {}
+ if isinstance(node, str):
+ params["system_id"] = node
+ elif isinstance(node, Node):
+ params["system_id"] = node.system_id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % (type(node).__name__)
+ )
+ if isinstance(cache_device, BlockDevice):
+ params["cache_device"] = cache_device.id
+ elif isinstance(cache_device, Partition):
+ params["cache_partition"] = cache_device.id
+ else:
+ raise TypeError(
+ "cache_device must be a BlockDevice or Partition, not %s"
+ % (type(cache_device).__name__)
+ )
+
+ return cls._object(await cls._handler.create(**params))
+
+
+class BcacheCacheSets(ObjectSet, metaclass=BcacheCacheSetsType):
+ """The set of cache sets on a machine."""
diff --git a/maas/client/viscera/bcaches.py b/maas/client/viscera/bcaches.py
new file mode 100644
index 00000000..c554852e
--- /dev/null
+++ b/maas/client/viscera/bcaches.py
@@ -0,0 +1,148 @@
+"""Objects for Bcaches."""
+
+__all__ = ["Bcache", "Bcaches"]
+
+from typing import Union
+
+from . import ObjectField, ObjectFieldRelated, ObjectSet, ObjectType, to, check
+from .nodes import Node
+from .bcache_cache_sets import BcacheCacheSet
+from .block_devices import BlockDevice
+from .partitions import Partition
+from .filesystem_groups import DeviceField, FilesystemGroup
+from ..enum import CacheMode
+
+
+class BcacheType(ObjectType):
+ """Metaclass for `Bcache`."""
+
+ async def read(cls, node, id):
+ """Get `Bcache` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ return cls(await cls._handler.read(system_id=system_id, id=id))
+
+
+class Bcache(FilesystemGroup, metaclass=BcacheType):
+ """A Bcache on a machine."""
+
+ cache_mode = ObjectField.Checked("cache_mode", to(CacheMode), check(CacheMode))
+ uuid = ObjectField.Checked("uuid", check(str), check(str))
+
+ backing_device = DeviceField("backing_device")
+ cache_set = ObjectFieldRelated("cache_set", "BcacheCacheSet", reverse=None)
+ virtual_device = ObjectFieldRelated(
+ "virtual_device", "BlockDevice", reverse=None, readonly=True
+ )
+
+ def __repr__(self):
+ return super(Bcache, self).__repr__(
+ fields={"name", "cache_mode", "size", "backing_device"}
+ )
+
+ async def delete(self):
+ """Delete this Bcache."""
+ await self._handler.delete(system_id=self.node.system_id, id=self.id)
+
+
+class BcachesType(ObjectType):
+ """Metaclass for `Bcaches`."""
+
+ async def read(cls, node):
+ """Get list of `Bcache`'s for `node`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ data = await cls._handler.read(system_id=system_id)
+ return cls(
+ cls._object(item, local_data={"node_system_id": system_id}) for item in data
+ )
+
+ async def create(
+ cls,
+ node: Union[Node, str],
+ name: str,
+ backing_device: Union[BlockDevice, Partition],
+ cache_set: Union[BcacheCacheSet, int],
+ cache_mode: CacheMode,
+ *,
+ uuid: str = None
+ ):
+ """
+ Create a Bcache on a Node.
+
+ :param node: Node to create the interface on.
+ :type node: `Node` or `str`
+ :param name: Name of the Bcache.
+ :type name: `str`
+ :param backing_device: Either a block device or partition to create
+ the Bcache from.
+ :type backing_device: `BlockDevice` or `Partition`
+ :param cache_set: Bcache cache set to use in front of backing device.
+ :type cache_set: `BcacheCacheSet` or `int`
+ :param cache_mode: Caching mode to use for this device.
+ :type cache_mode: `CacheMode`
+ :type backing_device: `BlockDevice` or `Partition`
+ :param uuid: The UUID for the Bcache (optional).
+ :type uuid: `str`
+ """
+ params = {"name": name}
+ if isinstance(node, str):
+ params["system_id"] = node
+ elif isinstance(node, Node):
+ params["system_id"] = node.system_id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % (type(node).__name__)
+ )
+
+ if isinstance(backing_device, BlockDevice):
+ params["backing_device"] = backing_device.id
+ elif isinstance(backing_device, Partition):
+ params["backing_partition"] = backing_device.id
+ else:
+ raise TypeError(
+ "backing_device must be a BlockDevice or Partition, "
+ "not %s" % type(backing_device).__name__
+ )
+
+ if isinstance(cache_set, BcacheCacheSet):
+ params["cache_set"] = cache_set.id
+ elif isinstance(cache_set, int):
+ params["cache_set"] = cache_set
+ else:
+ raise TypeError(
+ "cache_set must be a BcacheCacheSet or int, "
+ "not %s" % type(cache_set).__name__
+ )
+
+ if isinstance(cache_mode, CacheMode):
+ params["cache_mode"] = cache_mode.value
+ else:
+ raise TypeError(
+ "cache_mode must be a CacheMode, " "not %s" % type(cache_mode).__name__
+ )
+
+ if uuid is not None:
+ params["uuid"] = uuid
+ return cls._object(await cls._handler.create(**params))
+
+
+class Bcaches(ObjectSet, metaclass=BcachesType):
+ """The set of Bcaches on a machine."""
+
+ @property
+ def by_name(self):
+ """Return mapping of name to `Bcache`."""
+ return {bcache.name: bcache for bcache in self}
+
+ def get_by_name(self, name):
+ """Return a `Bcache` by its name."""
+ return self.by_name[name]
diff --git a/maas/client/viscera/block_devices.py b/maas/client/viscera/block_devices.py
new file mode 100644
index 00000000..c23df3ee
--- /dev/null
+++ b/maas/client/viscera/block_devices.py
@@ -0,0 +1,244 @@
+"""Objects for block devices."""
+
+__all__ = ["BlockDevice", "BlockDevices"]
+
+from typing import Iterable, Union
+
+from . import (
+ check,
+ check_optional,
+ Object,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ ObjectSet,
+ ObjectType,
+ to,
+)
+from .nodes import Node
+from ..enum import BlockDeviceType, PartitionTableType
+from ..utils import remove_None
+
+
+class BlockDeviceTypeMeta(ObjectType):
+ """Metaclass for `BlockDevice`."""
+
+ async def read(cls, node, id):
+ """Get `BlockDevice` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ return cls(await cls._handler.read(system_id=system_id, id=id))
+
+
+class BlockDevice(Object, metaclass=BlockDeviceTypeMeta):
+ """A block device on a machine."""
+
+ node = ObjectFieldRelated("system_id", "Node", readonly=True, pk=0)
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=1)
+ type = ObjectField.Checked("type", to(BlockDeviceType), readonly=True)
+ name = ObjectField.Checked("name", check(str), check(str), alt_pk=1)
+ model = ObjectField.Checked("model", check_optional(str), check_optional(str))
+ serial = ObjectField.Checked("serial", check_optional(str), check_optional(str))
+ id_path = ObjectField.Checked("id_path", check_optional(str), check_optional(str))
+ size = ObjectField.Checked("size", check(int), check(int))
+ block_size = ObjectField.Checked("block_size", check(int), check(int))
+ uuid = ObjectField.Checked("uuid", check(str), check(str))
+ tags = ObjectField.Checked("tags", check(list), check(list))
+
+ available_size = ObjectField.Checked("available_size", check(int), readonly=True)
+ used_size = ObjectField.Checked("used_size", check(int), readonly=True)
+ used_for = ObjectField.Checked("used_for", check(str), readonly=True)
+ partition_table_type = ObjectField.Checked(
+ "partition_table_type", to(PartitionTableType), readonly=True
+ )
+
+ partitions = ObjectFieldRelatedSet("partitions", "Partitions")
+ filesystem = ObjectFieldRelated("filesystem", "Filesystem", readonly=True)
+
+ def __repr__(self):
+ if self.type == BlockDeviceType.PHYSICAL:
+ return super(BlockDevice, self).__repr__(
+ name="PhysicalBlockDevice",
+ fields={"name", "model", "serial", "id_path"},
+ )
+ elif self.type == BlockDeviceType.VIRTUAL:
+ return super(BlockDevice, self).__repr__(
+ name="VirtualBlockDevice", fields={"name"}
+ )
+ else:
+ raise ValueError("Unknown type: %s" % self.type)
+
+ async def save(self):
+ """Save this block device."""
+ old_tags = list(self._orig_data["tags"])
+ new_tags = list(self.tags)
+ self._changed_data.pop("tags", None)
+ await super(BlockDevice, self).save()
+ for tag_name in new_tags:
+ if tag_name not in old_tags:
+ await self._handler.add_tag(
+ system_id=self.node.system_id, id=self.id, tag=tag_name
+ )
+ else:
+ old_tags.remove(tag_name)
+ for tag_name in old_tags:
+ await self._handler.remove_tag(
+ system_id=self.node.system_id, id=self.id, tag=tag_name
+ )
+ self._orig_data["tags"] = new_tags
+ self._data["tags"] = list(new_tags)
+
+ async def delete(self):
+ """Delete this block device."""
+ await self._handler.delete(system_id=self.node.system_id, id=self.id)
+
+ async def set_as_boot_disk(self):
+ """Set as boot disk for this node."""
+ await self._handler.set_boot_disk(system_id=self.node.system_id, id=self.id)
+
+ async def format(self, fstype, *, uuid=None):
+ """Format this block device."""
+ self._reset(
+ await self._handler.format(
+ system_id=self.node.system_id, id=self.id, fstype=fstype, uuid=uuid
+ )
+ )
+
+ async def unformat(self):
+ """Unformat this block device."""
+ self._reset(
+ await self._handler.unformat(system_id=self.node.system_id, id=self.id)
+ )
+
+ async def mount(self, mount_point, *, mount_options=None):
+ """Mount this block device."""
+ self._reset(
+ await self._handler.mount(
+ system_id=self.node.system_id,
+ id=self.id,
+ mount_point=mount_point,
+ mount_options=mount_options,
+ )
+ )
+
+ async def unmount(self):
+ """Unmount this block device."""
+ self._reset(
+ await self._handler.unmount(system_id=self.node.system_id, id=self.id)
+ )
+
+
+class BlockDevicesType(ObjectType):
+ """Metaclass for `BlockDevices`."""
+
+ async def read(cls, node):
+ """Get list of `BlockDevice`'s for `node`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ data = await cls._handler.read(system_id=system_id)
+ return cls(
+ cls._object(item, local_data={"node_system_id": system_id}) for item in data
+ )
+
+ async def create(
+ cls,
+ node: Union[Node, str],
+ name: str,
+ *,
+ model: str = None,
+ serial: str = None,
+ id_path: str = None,
+ size: int = None,
+ block_size: int = 512,
+ tags: Iterable[str] = None
+ ):
+ """
+ Create a physical block device on a Node.
+
+ Either model and serial or id_path must be provided when creating a
+ `BlockDevice`. Size (bytes) is always required.
+
+ NOTE: It is recommended to use the MAAS commissioning process to
+ discover `BlockDevice`'s on a machine. Getting any of this information
+ incorrect can result on the machine failing to deploy.
+
+ :param node: Node to create the block device on.
+ :type node: `Node` or `str`
+ :param name: The name for the block device.
+ :type name: `str`
+ :param model: The model number for the block device.
+ :type model: `str`
+ :param serial: The serial number for the block device.
+ :type serial: `str`
+ :param id_path: Unique path that identifies the device no matter
+ the kernel the machine boots. Use only when the block device
+ does not have a model and serial number.
+ :type id_path: `str`
+ :param size: The size of the block device in bytes.
+ :type size: `int`
+ :param block_size: The block size of the block device in bytes.
+ :type block_size: `int`
+ :param tags: List of tags to add to the block device.
+ :type tags: sequence of `str`
+ """
+ params = {}
+ if isinstance(node, str):
+ params["system_id"] = node
+ elif isinstance(node, Node):
+ params["system_id"] = node.system_id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % (type(node).__name__)
+ )
+
+ if not size or size < 0:
+ raise ValueError("size must be provided and greater than zero.")
+ if not block_size or block_size < 0:
+ raise ValueError("block_size must be provided and greater than zero.")
+ if model and not serial:
+ raise ValueError("serial must be provided when model is provided.")
+ if not model and serial:
+ raise ValueError("model must be provided when serial is provided.")
+ if not model and not serial and not id_path:
+ raise ValueError(
+ "Either model/serial is provided or id_path must be provided."
+ )
+
+ params.update(
+ remove_None(
+ {
+ "name": name,
+ "model": model,
+ "serial": serial,
+ "id_path": id_path,
+ "size": size,
+ "block_size": block_size,
+ }
+ )
+ )
+ device = cls._object(await cls._handler.create(**params))
+ if tags:
+ device.tags = tags
+ await device.save()
+ return device
+
+
+class BlockDevices(ObjectSet, metaclass=BlockDevicesType):
+ """The set of block devices on a machine."""
+
+ @property
+ def by_name(self):
+ """Return mapping of name of block device to `BlockDevice`."""
+ return {bd.name: bd for bd in self}
+
+ def get_by_name(self, name):
+ """Return a `BlockDevice` by its name."""
+ return self.by_name[name]
diff --git a/maas/client/viscera/boot_resources.py b/maas/client/viscera/boot_resources.py
index dfe616c2..2ab51c40 100644
--- a/maas/client/viscera/boot_resources.py
+++ b/maas/client/viscera/boot_resources.py
@@ -1,9 +1,6 @@
"""Objects for boot resources."""
-__all__ = [
- "BootResource",
- "BootResources",
-]
+__all__ = ["BootResource", "BootResources"]
import enum
import hashlib
@@ -23,7 +20,6 @@
)
from .. import utils
from ..bones import CallError
-from ..utils.typecheck import typed
def calc_size_and_sha265(content: io.IOBase, chunk_size: int):
@@ -50,31 +46,23 @@ class BootResourceFileType(enum.Enum):
class BootResourceFile(Object):
"""A boot resource file."""
- filename = ObjectField.Checked(
- "filename", check(str), readonly=True)
- filetype = ObjectField.Checked(
- "filetype", check(str), readonly=True)
- size = ObjectField.Checked(
- "size", check(int), readonly=True)
- sha256 = ObjectField.Checked(
- "sha256", check(str), readonly=True)
- complete = ObjectField.Checked(
- "complete", check(bool), readonly=True)
+ filename = ObjectField.Checked("filename", check(str), readonly=True)
+ filetype = ObjectField.Checked("filetype", check(str), readonly=True)
+ size = ObjectField.Checked("size", check(int), readonly=True)
+ sha256 = ObjectField.Checked("sha256", check(str), readonly=True)
+ complete = ObjectField.Checked("complete", check(bool), readonly=True)
class BootResourceSet(Object):
"""A boot resource set."""
- version = ObjectField.Checked(
- "version", check(str), readonly=True)
- size = ObjectField.Checked(
- "size", check(int), readonly=True)
- label = ObjectField.Checked(
- "label", check(str), readonly=True)
- complete = ObjectField.Checked(
- "complete", check(bool), readonly=True)
+ version = ObjectField.Checked("version", check(str), readonly=True)
+ size = ObjectField.Checked("size", check(int), readonly=True)
+ label = ObjectField.Checked("label", check(str), readonly=True)
+ complete = ObjectField.Checked("complete", check(bool), readonly=True)
files = ObjectField.Checked(
- "files", mapping_of(BootResourceFile), default=None, readonly=True)
+ "files", mapping_of(BootResourceFile), default=None, readonly=True
+ )
class BootResourcesType(ObjectType):
@@ -106,12 +94,17 @@ async def stop_import(cls):
"""Stop the import of `BootResource`'s."""
return cls._handler.stop_import()
- @typed
async def create(
- cls, name: str, architecture: str, content: io.IOBase, *,
- title: str="",
- filetype: BootResourceFileType=BootResourceFileType.TGZ,
- chunk_size=(1 << 22), progress_callback=None):
+ cls,
+ name: str,
+ architecture: str,
+ content: io.IOBase,
+ *,
+ title: str = "",
+ filetype: BootResourceFileType = BootResourceFileType.TGZ,
+ chunk_size=(1 << 22),
+ progress_callback=None
+ ):
"""Create a `BootResource`.
Creates an uploaded boot resource with `content`. The `content` is
@@ -141,24 +134,28 @@ async def create(
:returns: Create boot resource.
:rtype: `BootResource`.
"""
- if '/' not in name:
- raise ValueError(
- "name must be in format os/release; missing '/'")
- if '/' not in architecture:
- raise ValueError(
- "architecture must be in format arch/subarch; missing '/'")
+ if "/" not in name:
+ raise ValueError("name must be in format os/release; missing '/'")
+ if "/" not in architecture:
+ raise ValueError("architecture must be in format arch/subarch; missing '/'")
if not content.readable():
raise ValueError("content must be readable")
elif not content.seekable():
raise ValueError("content must be seekable")
if chunk_size <= 0:
- raise ValueError(
- "chunk_size must be greater than 0, not %d" % chunk_size)
+ raise ValueError("chunk_size must be greater than 0, not %d" % chunk_size)
size, sha256 = calc_size_and_sha265(content, chunk_size)
- resource = cls._object(await cls._handler.create(
- name=name, architecture=architecture, title=title,
- filetype=filetype.value, size=str(size), sha256=sha256))
+ resource = cls._object(
+ await cls._handler.create(
+ name=name,
+ architecture=architecture,
+ title=title,
+ filetype=filetype.value,
+ size=str(size),
+ sha256=sha256,
+ )
+ )
newest_set = max(resource.sets, default=None)
assert newest_set is not None
resource_set = resource.sets[newest_set]
@@ -169,18 +166,21 @@ async def create(
return resource
else:
# Upload in chunks and reload boot resource.
- await cls._upload_chunks(
- rfile, content, chunk_size, progress_callback)
+ await cls._upload_chunks(rfile, content, chunk_size, progress_callback)
return cls._object.read(resource.id)
- @typed
async def _upload_chunks(
- cls, rfile: BootResourceFile, content: io.IOBase, chunk_size: int,
- progress_callback=None):
+ cls,
+ rfile: BootResourceFile,
+ content: io.IOBase,
+ chunk_size: int,
+ progress_callback=None,
+ ):
"""Upload the `content` to `rfile` in chunks using `chunk_size`."""
content.seek(0, io.SEEK_SET)
- upload_uri = urlparse(
- cls._handler.uri)._replace(path=rfile._data['upload_uri']).geturl()
+ upload_uri = (
+ urlparse(cls._handler.uri)._replace(path=rfile._data["upload_uri"]).geturl()
+ )
uploaded_size = 0
insecure = cls._handler.session.insecure
@@ -199,32 +199,30 @@ async def _upload_chunks(
if length != chunk_size:
break
- @typed
async def _put_chunk(
- cls, session: aiohttp.ClientSession,
- upload_uri: str, buf: bytes):
+ cls, session: aiohttp.ClientSession, upload_uri: str, buf: bytes
+ ):
"""Upload one chunk to `upload_uri`."""
# Build the correct headers.
headers = {
- 'Content-Type': 'application/octet-stream',
- 'Content-Length': '%s' % len(buf),
+ "Content-Type": "application/octet-stream",
+ "Content-Length": "%s" % len(buf),
}
credentials = cls._handler.session.credentials
if credentials is not None:
utils.sign(upload_uri, headers, credentials)
# Perform upload of chunk.
- response = await session.put(
- upload_uri, data=buf, headers=headers)
- if response.status != 200:
- content = await response.read()
- request = {
- "body": buf,
- "headers": headers,
- "method": "PUT",
- "uri": upload_uri,
- }
- raise CallError(request, response, content, None)
+ async with await session.put(upload_uri, data=buf, headers=headers) as response:
+ if response.status != 200:
+ content = await response.read()
+ request = {
+ "body": buf,
+ "headers": headers,
+ "method": "PUT",
+ "uri": upload_uri,
+ }
+ raise CallError(request, response, content, None)
class BootResources(ObjectSet, metaclass=BootResourcesType):
@@ -232,7 +230,6 @@ class BootResources(ObjectSet, metaclass=BootResourcesType):
class BootResourceType(ObjectType):
-
async def read(cls, id: int):
"""Get `BootResource` by `id`."""
data = await cls._handler.read(id=id)
@@ -242,23 +239,27 @@ async def read(cls, id: int):
class BootResource(Object, metaclass=BootResourceType):
"""A boot resource."""
- id = ObjectField.Checked(
- "id", check(int), readonly=True)
- type = ObjectField.Checked(
- "type", check(str), check(str), readonly=True)
- name = ObjectField.Checked(
- "name", check(str), check(str), readonly=True)
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ type = ObjectField.Checked("type", check(str), check(str), readonly=True)
+ name = ObjectField.Checked("name", check(str), check(str), readonly=True)
architecture = ObjectField.Checked(
- "architecture", check(str), check(str), readonly=True)
+ "architecture", check(str), check(str), readonly=True
+ )
subarches = ObjectField.Checked(
- "subarches", check_optional(str), check_optional(str),
- default=None, readonly=True)
+ "subarches",
+ check_optional(str),
+ check_optional(str),
+ default=None,
+ readonly=True,
+ )
sets = ObjectField.Checked(
- "sets", mapping_of(BootResourceSet), default=None, readonly=True)
+ "sets", mapping_of(BootResourceSet), default=None, readonly=True
+ )
def __repr__(self):
return super(BootResource, self).__repr__(
- fields={"type", "name", "architecture"})
+ fields={"type", "name", "architecture"}
+ )
async def delete(self):
"""Delete boot resource."""
diff --git a/maas/client/viscera/boot_source_selections.py b/maas/client/viscera/boot_source_selections.py
index 74513f41..818e674f 100644
--- a/maas/client/viscera/boot_source_selections.py
+++ b/maas/client/viscera/boot_source_selections.py
@@ -1,19 +1,10 @@
"""Objects for boot source selections."""
-__all__ = [
- "BootSourceSelection",
- "BootSourceSelections",
-]
-
-from typing import List
-
-from . import (
- check,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
-)
+__all__ = ["BootSourceSelection", "BootSourceSelections"]
+
+
+from collections.abc import Sequence
+from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType
from .boot_sources import BootSource
@@ -21,35 +12,45 @@ class BootSourceSelectionsType(ObjectType):
"""Metaclass for `BootSourceSelections`."""
async def create(
- cls, boot_source, os, release, *,
- arches=None, subarches=None, labels=None):
+ cls, boot_source, os, release, *, arches=None, subarches=None, labels=None
+ ):
"""Create a new `BootSourceSelection`."""
if not isinstance(boot_source, BootSource):
raise TypeError(
- "boot_source must be a BootSource, not %s"
- % type(boot_source).__name__)
+ "boot_source must be a BootSource, not %s" % type(boot_source).__name__
+ )
if arches is None:
- arches = ['*']
+ arches = ["*"]
if subarches is None:
- subarches = ['*']
+ subarches = ["*"]
if labels is None:
- labels = ['*']
+ labels = ["*"]
data = await cls._handler.create(
boot_source_id=boot_source.id,
- os=os, release=release, arches=arches, subarches=subarches,
- labels=labels)
+ os=os,
+ release=release,
+ arches=arches,
+ subarches=subarches,
+ labels=labels,
+ )
return cls._object(data, {"boot_source_id": boot_source.id})
async def read(cls, boot_source):
"""Get list of `BootSourceSelection`'s."""
- if not isinstance(boot_source, BootSource):
+ if isinstance(boot_source, int):
+ boot_source_id = boot_source
+ elif isinstance(boot_source, BootSource):
+ boot_source_id = boot_source.id
+ else:
raise TypeError(
- "boot_source must be a BootSource, not %s"
- % type(boot_source).__name__)
- data = await cls._handler.read(boot_source_id=boot_source.id)
+ "boot_source must be a BootSource or int, not %s"
+ % type(boot_source).__name__
+ )
+ data = await cls._handler.read(boot_source_id=boot_source_id)
return cls(
- cls._object(item, local_data={"boot_source_id": boot_source.id})
- for item in data)
+ cls._object(item, local_data={"boot_source_id": boot_source_id})
+ for item in data
+ )
class BootSourceSelections(ObjectSet, metaclass=BootSourceSelectionsType):
@@ -57,15 +58,19 @@ class BootSourceSelections(ObjectSet, metaclass=BootSourceSelectionsType):
class BootSourceSelectionType(ObjectType):
-
async def read(cls, boot_source, id):
"""Get `BootSourceSelection` by `id`."""
- if not isinstance(boot_source, BootSource):
+ if isinstance(boot_source, int):
+ boot_source_id = boot_source
+ elif isinstance(boot_source, BootSource):
+ boot_source_id = boot_source.id
+ else:
raise TypeError(
- "boot_source must be a BootSource, not %s"
- % type(boot_source).__name__)
- data = await cls._handler.read(boot_source_id=boot_source.id, id=id)
- return cls(data, {"boot_source_id": boot_source.id})
+ "boot_source must be a BootSource or int, not %s"
+ % type(boot_source).__name__
+ )
+ data = await cls._handler.read(boot_source_id=boot_source_id, id=id)
+ return cls(data, {"boot_source_id": boot_source_id})
class BootSourceSelection(Object, metaclass=BootSourceSelectionType):
@@ -73,27 +78,28 @@ class BootSourceSelection(Object, metaclass=BootSourceSelectionType):
# Only client-side. Classes in this file place `boot_source_id` on
# the object using `local_data`.
- boot_source_id = ObjectField.Checked(
- "boot_source_id", check(int), readonly=True)
-
- id = ObjectField.Checked(
- "id", check(int), readonly=True)
- os = ObjectField.Checked(
- "os", check(str), check(str))
- release = ObjectField.Checked(
- "release", check(str), check(str))
- arches = ObjectField.Checked(
- "arches", check(List[str]), check(List[str]))
- subarches = ObjectField.Checked(
- "subarches", check(List[str]), check(List[str]))
- labels = ObjectField.Checked(
- "labels", check(List[str]), check(List[str]))
+ boot_source = ObjectFieldRelated(
+ "boot_source_id", "BootSource", readonly=True, pk=0
+ )
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=1)
+ os = ObjectField.Checked("os", check(str), check(str))
+ release = ObjectField.Checked("release", check(str), check(str))
+ arches = ObjectField.Checked( # List[str]
+ "arches", check(Sequence), check(Sequence)
+ )
+ subarches = ObjectField.Checked( # List[str]
+ "subarches", check(Sequence), check(Sequence)
+ )
+ labels = ObjectField.Checked( # List[str]
+ "labels", check(Sequence), check(Sequence)
+ )
def __repr__(self):
return super(BootSourceSelection, self).__repr__(
- fields={"os", "release", "arches", "subarches", "labels"})
+ fields={"os", "release", "arches", "subarches", "labels"}
+ )
async def delete(self):
"""Delete boot source selection."""
- await self._handler.delete(
- boot_source_id=self.boot_source_id, id=self.id)
+ await self._handler.delete(boot_source_id=self.boot_source.id, id=self.id)
diff --git a/maas/client/viscera/boot_sources.py b/maas/client/viscera/boot_sources.py
index 05b8f7c5..d5daaf3f 100644
--- a/maas/client/viscera/boot_sources.py
+++ b/maas/client/viscera/boot_sources.py
@@ -1,37 +1,28 @@
"""Objects for boot sources."""
-__all__ = [
- "BootSource",
- "BootSources",
-]
+__all__ = ["BootSource", "BootSources"]
-from . import (
- check,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
- parse_timestamp,
-)
+from . import check, Object, ObjectField, ObjectSet, ObjectType, parse_timestamp
+from ..utils import coalesce
class BootSourcesType(ObjectType):
"""Metaclass for `BootSources`."""
async def create(cls, url, *, keyring_filename=None, keyring_data=None):
- """Create a new `BootSource`."""
- if (not url.endswith(".json") and
- keyring_filename is None and
- keyring_data is None):
- raise ValueError(
- "Either keyring_filename and keyring_data must be set when "
- "providing a signed source.")
+ """Create a new `BootSource`.
+
+ :param url: The URL for the boot source.
+ :param keyring_filename: The path to the keyring file on the server.
+ :param keyring_data: The GPG keyring data, binary. as a file-like
+ object. For example: an open file handle in binary mode, or an
+ instance of `io.BytesIO`.
+ """
data = await cls._handler.create(
url=url,
- keyring_filename=(
- "" if keyring_filename is None else keyring_filename),
- keyring_data=(
- "" if keyring_data is None else keyring_data))
+ keyring_filename=coalesce(keyring_filename, ""),
+ keyring_data=coalesce(keyring_data, ""),
+ )
return cls._object(data)
async def read(cls):
@@ -45,7 +36,6 @@ class BootSources(ObjectSet, metaclass=BootSourcesType):
class BootSourceType(ObjectType):
-
async def read(cls, id):
"""Get `BootSource` by `id`."""
data = await cls._handler.read(id=id)
@@ -55,22 +45,21 @@ async def read(cls, id):
class BootSource(Object, metaclass=BootSourceType):
"""A boot source."""
- id = ObjectField.Checked(
- "id", check(int), readonly=True)
- url = ObjectField.Checked(
- "url", check(str), check(str))
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ url = ObjectField.Checked("url", check(str), check(str))
keyring_filename = ObjectField.Checked(
- "keyring_filename", check(str), check(str), default="")
+ "keyring_filename", check(str), check(str), default=""
+ )
keyring_data = ObjectField.Checked(
- "keyring_data", check(str), check(str), default="")
- created = ObjectField.Checked(
- "created", parse_timestamp, readonly=True)
- updated = ObjectField.Checked(
- "updated", parse_timestamp, readonly=True)
+ "keyring_data", check(str), check(str), default=""
+ )
+ created = ObjectField.Checked("created", parse_timestamp, readonly=True)
+ updated = ObjectField.Checked("updated", parse_timestamp, readonly=True)
def __repr__(self):
return super(BootSource, self).__repr__(
- fields={"url", "keyring_filename", "keyring_data"})
+ fields={"url", "keyring_filename", "keyring_data"}
+ )
async def delete(self):
"""Delete boot source."""
diff --git a/maas/client/viscera/controllers.py b/maas/client/viscera/controllers.py
index d657650a..83fc66be 100644
--- a/maas/client/viscera/controllers.py
+++ b/maas/client/viscera/controllers.py
@@ -1,110 +1,65 @@
"""Objects for region and rack controllers."""
-__all__ = [
- "RackController",
- "RackControllers",
-]
+__all__ = ["RackController", "RackControllers", "RegionController", "RegionControllers"]
-from typing import List
+from . import check, check_optional, ObjectField, to
+from .nodes import Node, Nodes, NodesType, NodeTypeMeta
+from ..enum import PowerState
-from . import (
- check,
- check_optional,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
- zones,
-)
-
-class RackControllersType(ObjectType):
+class RackControllersType(NodesType):
"""Metaclass for `RackControllers`."""
- async def read(cls):
- data = await cls._handler.read()
- return cls(map(cls._object, data))
-
-
-class RackControllerNotFound(Exception):
- """Rack-controller was not found."""
-
-class RackControllers(ObjectSet, metaclass=RackControllersType):
+class RackControllers(Nodes, metaclass=RackControllersType):
"""The set of rack-controllers stored in MAAS."""
-class RackControllerType(ObjectType):
+class RackControllerType(NodeTypeMeta):
+ """Metaclass for `RackController`."""
- async def read(cls, system_id):
- data = await cls._handler.read(system_id=system_id)
- return cls(data)
-
-class RackController(Object, metaclass=RackControllerType):
+class RackController(Node, metaclass=RackControllerType):
"""A rack-controller stored in MAAS."""
architecture = ObjectField.Checked(
- "architecture", check_optional(str), check_optional(str))
- boot_disk = ObjectField.Checked(
- "boot_disk", check_optional(str), check_optional(str))
- cpus = ObjectField.Checked(
- "cpu_count", check(int), check(int))
- disable_ipv4 = ObjectField.Checked(
- "disable_ipv4", check(bool), check(bool))
- distro_series = ObjectField.Checked(
- "distro_series", check(str), check(str))
- hostname = ObjectField.Checked(
- "hostname", check(str), check(str))
- hwe_kernel = ObjectField.Checked(
- "hwe_kernel", check_optional(str), check_optional(str))
- ip_addresses = ObjectField.Checked(
- "ip_addresses", check(List[str]), readonly=True)
- memory = ObjectField.Checked(
- "memory", check(int), check(int))
- min_hwe_kernel = ObjectField.Checked(
- "min_hwe_kernel", check_optional(str), check_optional(str))
-
- # blockdevice_set
- # interface_set
- # macaddress_set
- # netboot
- # osystem
- # owner
- # physicalblockdevice_set
-
- # TODO: Use an enum here.
- power_state = ObjectField.Checked(
- "power_state", check(str), readonly=True)
+ "architecture", check_optional(str), check_optional(str)
+ )
+ cpus = ObjectField.Checked("cpu_count", check(int), check(int))
+ distro_series = ObjectField.Checked("distro_series", check(str), check(str))
+ memory = ObjectField.Checked("memory", check(int), check(int))
+ osystem = ObjectField.Checked("osystem", check(str), readonly=True)
+ power_state = ObjectField.Checked("power_state", to(PowerState), readonly=True)
# power_type
- # pxe_mac
- # resource_uri
- # routers
- # status
- # storage
-
- status = ObjectField.Checked(
- "status", check(int), readonly=True)
- status_action = ObjectField.Checked(
- "substatus_action", check_optional(str), readonly=True)
- status_message = ObjectField.Checked(
- "substatus_message", check_optional(str), readonly=True)
- status_name = ObjectField.Checked(
- "substatus_name", check(str), readonly=True)
-
+ # service_set
# swap_size
- system_id = ObjectField.Checked(
- "system_id", check(str), readonly=True)
- tags = ObjectField.Checked(
- "tag_names", check(List[str]), readonly=True)
- # virtualblockdevice_set
+class RegionControllersType(NodesType):
+ """Metaclass for `RegionControllers`."""
+
+
+class RegionControllers(Nodes, metaclass=RegionControllersType):
+ """The set of region-controllers stored in MAAS."""
- zone = zones.ZoneField(
- "zone", readonly=True)
- # def __repr__(self):
- # return super(RackController, self).__repr__(
- # fields={"system_id", "hostname"})
+class RegionControllerType(NodeTypeMeta):
+ """Metaclass for `RegionController`."""
+
+
+class RegionController(Node, metaclass=RegionControllerType):
+ """A region-controller stored in MAAS."""
+
+ architecture = ObjectField.Checked(
+ "architecture", check_optional(str), check_optional(str)
+ )
+ cpus = ObjectField.Checked("cpu_count", check(int), check(int))
+ distro_series = ObjectField.Checked("distro_series", check(str), check(str))
+ memory = ObjectField.Checked("memory", check(int), check(int))
+ osystem = ObjectField.Checked("osystem", check(str), readonly=True)
+ power_state = ObjectField.Checked("power_state", to(PowerState), readonly=True)
+
+ # power_type
+ # service_set
+ # swap_size
diff --git a/maas/client/viscera/devices.py b/maas/client/viscera/devices.py
index c9367b46..ee74f75f 100644
--- a/maas/client/viscera/devices.py
+++ b/maas/client/viscera/devices.py
@@ -1,63 +1,59 @@
"""Objects for devices."""
-__all__ = [
- "Device",
- "Devices",
-]
+__all__ = ["Device", "Devices"]
-from typing import List
+import typing
-from . import (
- check,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
- zones,
-)
+from .nodes import Node, Nodes, NodesType, NodeTypeMeta
+from .zones import Zone
-class DevicesType(ObjectType):
+class DevicesType(NodesType):
"""Metaclass for `Devices`."""
- async def read(cls):
- data = await cls._handler.read()
- return cls(map(cls._object, data))
-
-
-class DeviceNotFound(Exception):
- """Device was not found."""
-
-
-class Devices(ObjectSet, metaclass=DevicesType):
+ async def create(
+ cls,
+ mac_addresses: typing.Sequence[str],
+ hostname: str = None,
+ domain: typing.Union[int, str] = None,
+ zone: typing.Union[str, Zone] = None,
+ ):
+ """Create a new device.
+
+ :param mac_addresses: The MAC address(es) of the device (required).
+ :type mac_addresses: sequence of `str`
+ :param hostname: The hostname for the device (optional).
+ :type hostname: `str`
+ :param domain: The domain for the device (optional).
+ :type domain: `int` or `str`
+ :param zone: The zone for the device (optional).
+ :type zone: `Zone` or `str`
+
+ """
+ params = {"mac_addresses": mac_addresses}
+ if hostname is not None:
+ params["hostname"] = hostname
+ if domain is not None:
+ params["domain"] = domain
+ if zone is not None:
+ if isinstance(zone, Zone):
+ params["zone"] = zone.name
+ elif isinstance(zone, str):
+ params["zone"] = zone
+ else:
+ raise TypeError(
+ "zone must be a str or Zone, not %s" % type(zone).__name__
+ )
+ return cls._object(await cls._handler.create(**params))
+
+
+class Devices(Nodes, metaclass=DevicesType):
"""The set of devices stored in MAAS."""
-class DeviceType(ObjectType):
-
- async def read(cls, system_id):
- data = await cls._handler.read(system_id=system_id)
- return cls(data)
+class DeviceType(NodeTypeMeta):
+ """Metaclass for `Device`."""
-class Device(Object, metaclass=DeviceType):
+class Device(Node, metaclass=DeviceType):
"""A device stored in MAAS."""
-
- hostname = ObjectField.Checked(
- "hostname", check(str), check(str))
- ip_addresses = ObjectField.Checked(
- "ip_addresses", check(List[str]), readonly=True)
-
- # owner
- # resource_uri
-
- system_id = ObjectField.Checked(
- "system_id", check(str), readonly=True)
- tags = ObjectField.Checked(
- "tag_names", check(List[str]), readonly=True)
- zone = zones.ZoneField(
- "zone", readonly=True)
-
- def __repr__(self):
- return super(Device, self).__repr__(
- fields={"system_id", "hostname"})
diff --git a/maas/client/viscera/dnsresourcerecords.py b/maas/client/viscera/dnsresourcerecords.py
new file mode 100644
index 00000000..4aae103c
--- /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 00000000..97e1e46b
--- /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/domains.py b/maas/client/viscera/domains.py
new file mode 100644
index 00000000..43c96f4d
--- /dev/null
+++ b/maas/client/viscera/domains.py
@@ -0,0 +1,61 @@
+"""Objects for domains."""
+
+__all__ = ["Domain", "Domains"]
+
+from . import check, check_optional, Object, ObjectField, ObjectSet, ObjectType
+
+
+class DomainType(ObjectType):
+ """Metaclass for `Domains`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(cls, name: str, authoritative: bool = True, ttl: int = None):
+ """
+ Create a `Domain` in MAAS.
+
+ :param name: The name of the `Domain`.
+ :type name: `str`
+ :param authoritative: Whether the domain is authoritative.
+ :type authoritative: `bool`
+ :param ttl: Optional TTL for the domain.
+ :type ttl: `int`
+ :returns: The created `Domain`
+ :rtype: `Domain`
+ """
+ params = {"name": name, "authoritative": authoritative}
+ if ttl is not None:
+ params["ttl"] = ttl
+ return cls._object(await cls._handler.create(**params))
+
+
+class Domains(ObjectSet, metaclass=DomainType):
+ """The set of domains stored in MAAS."""
+
+
+class DomainType(ObjectType):
+ async def read(cls, id):
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class Domain(Object, metaclass=DomainType):
+ """A domain stored in MAAS."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ name = ObjectField.Checked("name", check(str), check(str))
+ authoritative = ObjectField.Checked(
+ "authoritative", check_optional(bool), check_optional(bool)
+ )
+ ttl = ObjectField.Checked("ttl", check_optional(int), check_optional(int))
+
+ def __repr__(self):
+ return super(Domain, self).__repr__(fields={"name", "authoritative", "ttl"})
+
+ async def delete(self):
+ """
+ Deletes the `domain` from MAAS.
+ """
+ return await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/events.py b/maas/client/viscera/events.py
index 4c42a6d4..ac3f31fc 100644
--- a/maas/client/viscera/events.py
+++ b/maas/client/viscera/events.py
@@ -1,32 +1,19 @@
"""Objects for events."""
-__all__ = [
- "Events",
-]
+__all__ = ["Events"]
from datetime import datetime
import enum
from functools import partial
import logging
-from typing import (
- Iterable,
- Union,
-)
-from urllib.parse import (
- parse_qs,
- urlparse,
-)
+import typing
+from urllib.parse import parse_qs, urlparse
+from .users import User
import pytz
-from . import (
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
-)
-from ..utils.async import is_loop_running
-from ..utils.typecheck import typed
+from . import Object, ObjectField, ObjectSet, ObjectType
+from ..utils.maas_async import is_loop_running
#
# The query API call returns:
@@ -47,7 +34,8 @@
# level=event.type.level_str,
# created=event.created.strftime('%a, %d %b. %Y %H:%M:%S'),
# type=event.type.description,
-# description=event.description
+# description=event.description,
+# username=event.user.username
# )
#
# Notes:
@@ -61,6 +49,7 @@ class Level(enum.IntEnum):
They happen to correspond to levels in the `logging` module.
"""
+ AUDIT = 0
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
@@ -86,19 +75,21 @@ class EventsType(ObjectType):
Level = Level
- @typed
async def query(
- cls, *,
- hostnames: Iterable[str]=None,
- domains: Iterable[str]=None,
- zones: Iterable[str]=None,
- macs: Iterable[str]=None,
- system_ids: Iterable[str]=None,
- agent_name: str=None,
- level: Union[Level, int, str]=None,
- before: int=None,
- after: int=None,
- limit: int=None):
+ cls,
+ *,
+ hostnames: typing.Iterable[str] = None,
+ domains: typing.Iterable[str] = None,
+ zones: typing.Iterable[str] = None,
+ macs: typing.Iterable[str] = None,
+ system_ids: typing.Iterable[str] = None,
+ agent_name: str = None,
+ level: typing.Union[Level, int, str] = None,
+ before: int = None,
+ after: int = None,
+ limit: int = None,
+ owner: typing.Union[User, str] = None
+ ):
"""Query MAAS for matching events."""
if before is not None and after is not None:
@@ -127,6 +118,15 @@ async def query(
params["after"] = ["{:d}".format(after)]
if limit is not None:
params["limit"] = ["{:d}".format(limit)]
+ if owner is not None:
+ if isinstance(owner, User):
+ params["owner"] = [owner.username]
+ elif isinstance(owner, str):
+ params["owner"] = [owner]
+ else:
+ raise TypeError(
+ "owner must be either User or str, not %s" % (type(owner).__name__)
+ )
data = await cls._handler.query(**params)
return cls(data)
@@ -197,8 +197,8 @@ def _backwards_sync(self):
yield from current
if is_loop_running():
raise RuntimeError(
- "Cannot iterate synchronously while "
- "event-loop is running.")
+ "Cannot iterate synchronously while " "event-loop is running."
+ )
current = current.prev()
def forwards(self):
@@ -219,13 +219,12 @@ def _forwards_sync(self):
yield from reversed(current)
if is_loop_running():
raise RuntimeError(
- "Cannot iterate synchronously while "
- "event-loop is running.")
+ "Cannot iterate synchronously while " "event-loop is running."
+ )
current = current.next()
class EventsAsyncIteratorBackwards:
-
def __init__(self, current):
super(EventsAsyncIteratorBackwards, self).__init__()
self._current_iter = iter(current)
@@ -247,7 +246,6 @@ async def __anext__(self):
class EventsAsyncIteratorForwards:
-
def __init__(self, current):
super(EventsAsyncIteratorForwards, self).__init__()
self._current_iter = reversed(current)
@@ -276,7 +274,7 @@ def truncate(length, text): # TODO: Move into utils.
Otherwise return the given text unaltered.
"""
if len(text) > length:
- return text[:length - 1] + "…"
+ return text[: length - 1] + "…"
else:
return text
@@ -284,25 +282,21 @@ def truncate(length, text): # TODO: Move into utils.
class Event(Object):
"""An event."""
- event_id = ObjectField(
- "id", readonly=True)
- event_type = ObjectField(
- "type", readonly=True)
+ event_id = ObjectField("id", readonly=True)
+ event_type = ObjectField("type", readonly=True)
- system_id = ObjectField(
- "node", readonly=True)
- hostname = ObjectField(
- "hostname", readonly=True)
+ system_id = ObjectField("node", readonly=True)
+ hostname = ObjectField("hostname", readonly=True)
- level = ObjectField.Checked(
- "level", Level.normalise, readonly=True)
- created = ObjectField.Checked(
- "created", parse_created_timestamp, readonly=True)
+ level = ObjectField.Checked("level", Level.normalise, readonly=True)
+ created = ObjectField.Checked("created", parse_created_timestamp, readonly=True)
- description = ObjectField(
- "description", readonly=True)
+ description = ObjectField("description", readonly=True)
description_short = ObjectField.Checked(
- "description", partial(truncate, 50), readonly=True)
+ "description", partial(truncate, 50), readonly=True
+ )
+
+ username = ObjectField("username", readonly=True)
def __repr__(self):
return (
diff --git a/maas/client/viscera/fabrics.py b/maas/client/viscera/fabrics.py
new file mode 100644
index 00000000..16896a9e
--- /dev/null
+++ b/maas/client/viscera/fabrics.py
@@ -0,0 +1,75 @@
+"""Objects for fabrics."""
+
+__all__ = ["Fabrics", "Fabric"]
+
+from . import check, Object, ObjectField, ObjectFieldRelatedSet, ObjectSet, ObjectType
+from ..errors import CannotDelete
+
+
+class FabricsType(ObjectType):
+ """Metaclass for `Fabrics`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(
+ cls, *, name: str = None, description: str = None, class_type: str = None
+ ):
+ """
+ Create a `Fabric` in MAAS.
+
+ :param name: The name of the `Fabric` (optional, will be given a
+ default value if not specified).
+ :type name: `str`
+ :param description: A description of the `Fabric` (optional).
+ :type description: `str`
+ :param class_type: The class type of the `Fabric` (optional).
+ :type class_type: `str`
+ :returns: The created Fabric
+ :rtype: `Fabric`
+ """
+ params = {}
+ if name is not None:
+ params["name"] = name
+ if description is not None:
+ params["description"] = description
+ if class_type is not None:
+ params["class_type"] = class_type
+ return cls._object(await cls._handler.create(**params))
+
+
+class Fabrics(ObjectSet, metaclass=FabricsType):
+ """The set of Fabrics stored in MAAS."""
+
+
+class FabricType(ObjectType):
+ """Metaclass for `Fabric`."""
+
+ _default_fabric_id = 0
+
+ async def get_default(cls):
+ """
+ Get the 'default' Fabric for the MAAS.
+ """
+ data = await cls._handler.read(id=cls._default_fabric_id)
+ return cls(data)
+
+ async def read(cls, id: int):
+ """Get a `Fabric` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class Fabric(Object, metaclass=FabricType):
+ """A Fabric."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ name = ObjectField.Checked("name", check(str), check(str))
+ vlans = ObjectFieldRelatedSet("vlans", "Vlans")
+
+ async def delete(self):
+ """Delete this Fabric."""
+ if self.id == self._origin.Fabric._default_fabric_id:
+ raise CannotDelete("Default fabric cannot be deleted.")
+ await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/files.py b/maas/client/viscera/files.py
index 3c7d0652..e11835a0 100644
--- a/maas/client/viscera/files.py
+++ b/maas/client/viscera/files.py
@@ -1,17 +1,8 @@
"""Objects for files."""
-__all__ = [
- "File",
- "Files",
-]
+__all__ = ["File", "Files"]
-from . import (
- check,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
-)
+from . import check, Object, ObjectField, ObjectSet, ObjectType
class FilesType(ObjectType):
@@ -29,5 +20,4 @@ class Files(ObjectSet, metaclass=FilesType):
class File(Object):
"""A file stored in MAAS."""
- filename = ObjectField.Checked(
- "filename", check(str), readonly=True)
+ filename = ObjectField.Checked("filename", check(str), readonly=True)
diff --git a/maas/client/viscera/filesystem_groups.py b/maas/client/viscera/filesystem_groups.py
new file mode 100644
index 00000000..41c6738d
--- /dev/null
+++ b/maas/client/viscera/filesystem_groups.py
@@ -0,0 +1,98 @@
+"""Base object for all filesystem group objects."""
+
+__all__ = ["FilesystemGroup"]
+
+from typing import Sequence
+
+from . import (
+ check,
+ Object,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ ObjectSet,
+ undefined,
+)
+from ..enum import BlockDeviceType
+
+
+def get_device_object(origin, datum):
+ device_type = datum.get("type")
+ if device_type in [BlockDeviceType.PHYSICAL.value, BlockDeviceType.VIRTUAL.value]:
+ return origin.BlockDevice(datum)
+ elif device_type == "partition":
+ return origin.Partition(datum)
+ else:
+ raise ValueError("Unknown devices type: %s" % device_type)
+
+
+class FilesystemGroupDevices(ObjectSet):
+ """Devices that make up a `FilesystemGroup`."""
+
+
+class DevicesField(ObjectFieldRelatedSet):
+ """Field for `FilesystemGroupDevices`."""
+
+ def __init__(self, name):
+ """Create a `DevicesField`.
+
+ :param name: The name of the field. This is the name that's used to
+ store the datum in the MAAS-side data dictionary.
+ """
+ super(ObjectFieldRelatedSet, self).__init__(name, default=[], readonly=True)
+
+ def datum_to_value(self, instance, datum):
+ """Convert a given MAAS-side datum to a Python-side value.
+
+ :param instance: The `Object` instance on which this field is
+ currently operating. This method should treat it as read-only, for
+ example to perform validation with regards to other fields.
+ :param datum: The MAAS-side datum to validate and convert into a
+ Python-side value.
+ :return: A set of `cls` from the given datum.
+ """
+ if datum is None:
+ return []
+ if not isinstance(datum, Sequence):
+ raise TypeError("datum must be a sequence, not %s" % type(datum).__name__)
+ # Get the class from the bound origin.
+ bound = getattr(instance._origin, "FilesystemGroupDevices")
+ return bound((get_device_object(instance._origin, item) for item in datum))
+
+
+class DeviceField(ObjectFieldRelated):
+ """Field that returns either `BlockDevice` or `Partition`."""
+
+ def __init__(self, name, readonly=False):
+ """Create a `DevicesField`.
+
+ :param name: The name of the field. This is the name that's used to
+ store the datum in the MAAS-side data dictionary.
+ """
+ super(ObjectFieldRelated, self).__init__(
+ name, default=undefined, readonly=readonly
+ )
+
+ def datum_to_value(self, instance, datum):
+ """Convert a given MAAS-side datum to a Python-side value.
+
+ :param instance: The `Object` instance on which this field is
+ currently operating. This method should treat it as read-only, for
+ example to perform validation with regards to other fields.
+ :param datum: The MAAS-side datum to validate and convert into a
+ Python-side value.
+ :return: A set of `cls` from the given datum.
+ """
+ return get_device_object(instance._origin, datum)
+
+
+class FilesystemGroup(Object):
+ """A filesystem group on a machine.
+
+ Used by `CacheSet`, `Bcache`, `Raid`, and `VolumeGroup`. Never use
+ directly.
+ """
+
+ node = ObjectFieldRelated("system_id", "Node", readonly=True, pk=0)
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=1)
+ name = ObjectField.Checked("name", check(str), check(str), alt_pk=1)
diff --git a/maas/client/viscera/filesystems.py b/maas/client/viscera/filesystems.py
new file mode 100644
index 00000000..89df5295
--- /dev/null
+++ b/maas/client/viscera/filesystems.py
@@ -0,0 +1,18 @@
+"""Objects for filesystems."""
+
+__all__ = ["Filesystem"]
+
+from . import check, Object, ObjectField
+
+
+class Filesystem(Object):
+ """A filesystem on either a partition or block device."""
+
+ label = ObjectField.Checked("label", check(str), readonly=True)
+ fstype = ObjectField.Checked("fstype", check(str), readonly=True)
+ mount_point = ObjectField.Checked("mount_point", check(str), readonly=True)
+ mount_options = ObjectField.Checked("mount_options", check(str), readonly=True)
+ uuid = ObjectField.Checked("uuid", check(str), readonly=True)
+
+ def __repr__(self):
+ return super(Filesystem, self).__repr__(fields={"fstype", "mount_point"})
diff --git a/maas/client/viscera/interfaces.py b/maas/client/viscera/interfaces.py
new file mode 100644
index 00000000..c9d2a22e
--- /dev/null
+++ b/maas/client/viscera/interfaces.py
@@ -0,0 +1,500 @@
+"""Objects for interfaces."""
+
+__all__ = ["Interface", "Interfaces"]
+
+import copy
+from typing import Iterable, Union
+
+from . import (
+ check,
+ Object,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ ObjectSet,
+ ObjectType,
+ to,
+)
+from .nodes import Node
+from .subnets import Subnet
+from .vlans import Vlan
+from ..enum import InterfaceType, LinkMode
+from ..utils.diff import calculate_dict_diff
+
+
+def gen_parents(parents):
+ """Generate the parents to send to the handler."""
+ for idx, parent in enumerate(parents):
+ if isinstance(parent, Interface):
+ parent = parent.id
+ elif isinstance(parent, int):
+ pass
+ else:
+ raise TypeError(
+ "parent[%d] must be an Interface or int, not %s"
+ % (idx, type(parent).__name__)
+ )
+ yield parent
+
+
+def get_parent(parent):
+ """Get the parent to send to the handler."""
+ if isinstance(parent, Interface):
+ return parent.id
+ elif isinstance(parent, int):
+ return parent
+ else:
+ raise TypeError(
+ "parent must be an Interface or int, not %s" % (type(parent).__name__)
+ )
+
+
+class InterfaceTypeMeta(ObjectType):
+ """Metaclass for `Interface`."""
+
+ async def read(cls, node, id):
+ """Get `Interface` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ return cls(await cls._handler.read(system_id=system_id, id=id))
+
+
+def map_nic_name_to_dict(instance, value):
+ """Convert a name of interface into a dictionary.
+
+ `parents` and `children` just hold a list of interface names. This is need
+ so instead they can return a `ObjectSet`.
+
+ '__incomplete__' is set so the object knows that the data passed is
+ incomplete data.
+ """
+ return {
+ "system_id": instance._data["system_id"],
+ "name": value,
+ "__incomplete__": True,
+ }
+
+
+class Interface(Object, metaclass=InterfaceTypeMeta):
+ """A interface on a machine."""
+
+ node = ObjectFieldRelated("system_id", "Node", readonly=True, pk=0)
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=1)
+ type = ObjectField.Checked("type", to(InterfaceType), readonly=True)
+ name = ObjectField.Checked("name", check(str), check(str), alt_pk=1)
+ mac_address = ObjectField.Checked("mac_address", check(str), check(str))
+ enabled = ObjectField.Checked("enabled", check(bool), check(bool))
+ effective_mtu = ObjectField.Checked("effective_mtu", check(int), readonly=True)
+ tags = ObjectField.Checked("tags", check(list), check(list))
+ params = ObjectField.Checked("params", check((dict, str)), check((dict, str)))
+ parents = ObjectFieldRelatedSet(
+ "parents", "Interfaces", reverse=None, map_func=map_nic_name_to_dict
+ )
+ children = ObjectFieldRelatedSet(
+ "children", "Interfaces", reverse=None, map_func=map_nic_name_to_dict
+ )
+ vlan = ObjectFieldRelated("vlan", "Vlan", reverse=None, use_data_setter=True)
+ links = ObjectFieldRelatedSet("links", "InterfaceLinks", reverse="interface")
+ 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"})
+
+ async def save(self):
+ """Save this interface."""
+ if set(self.tags) != set(self._orig_data["tags"]):
+ self._changed_data["tags"] = ",".join(self.tags)
+ elif "tags" in self._changed_data:
+ del self._changed_data["tags"]
+ orig_params = self._orig_data["params"]
+ if not isinstance(orig_params, dict):
+ orig_params = {}
+ params = self.params
+ if not isinstance(params, dict):
+ params = {}
+ self._changed_data.pop("params", None)
+ self._changed_data.update(calculate_dict_diff(orig_params, params))
+ if "vlan" in self._changed_data and self._changed_data["vlan"]:
+ # Update uses the ID of the VLAN, not the VLAN object.
+ self._changed_data["vlan"] = self._changed_data["vlan"]["id"]
+ if (
+ self._orig_data["vlan"]
+ and "id" in self._orig_data["vlan"]
+ and self._changed_data["vlan"] == (self._orig_data["vlan"]["id"])
+ ):
+ # VLAN didn't really change, the object was just set to the
+ # same VLAN.
+ del self._changed_data["vlan"]
+ await super(Interface, self).save()
+
+ async def delete(self):
+ """Delete this interface."""
+ await self._handler.delete(system_id=self.node.system_id, id=self.id)
+
+ async def disconnect(self):
+ """Disconnect this interface."""
+ self._reset(
+ await self._handler.disconnect(system_id=self.node.system_id, id=self.id)
+ )
+
+
+class InterfaceDiscoveredLink(Object):
+ """Discovered link information on an `Interface`."""
+
+ ip_address = ObjectField.Checked(
+ "ip_address", check(str), readonly=True, default=None
+ )
+ subnet = ObjectFieldRelated("subnet", "Subnet", readonly=True, default=None)
+
+
+class InterfaceDiscoveredLinks(ObjectSet):
+ """A set of discovered links on an `Interface`."""
+
+
+class InterfaceLink(Object):
+ """A link on an `Interface`."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True)
+ mode = ObjectField.Checked("mode", to(LinkMode), readonly=True)
+ subnet = ObjectFieldRelated("subnet", "Subnet", readonly=True, default=None)
+ ip_address = ObjectField.Checked(
+ "ip_address", check(str), readonly=True, default=None
+ )
+
+ def __repr__(self):
+ return super(InterfaceLink, self).__repr__(
+ fields={"mode", "ip_address", "subnet"}
+ )
+
+ async def delete(self):
+ """Delete this interface link."""
+ interface = self._data["interface"]
+ data = await interface._handler.unlink_subnet(
+ system_id=interface.node.system_id, id=interface.id, _id=self.id
+ )
+ interface._data["links"] = list(data["links"])
+ interface._orig_data["links"] = copy.deepcopy(interface._data["links"])
+
+ async def set_as_default_gateway(self):
+ """Set this link as the default gateway for the node."""
+ interface = self._data["interface"]
+ await interface._handler.set_default_gateway(
+ system_id=interface.node.system_id, id=interface.id, link_id=self.id
+ )
+
+
+class InterfaceLinksType(ObjectType):
+ """Metaclass for `InterfaceLinks`."""
+
+ async def create(
+ cls,
+ interface: Interface,
+ mode: LinkMode,
+ subnet: Union[Subnet, int] = None,
+ ip_address: str = None,
+ force: bool = False,
+ default_gateway: bool = False,
+ ):
+ """
+ Create a link on `Interface` in MAAS.
+
+ :param interface: Interface to create the link on.
+ :type interface: `Interface`
+ :param mode: Mode of the link.
+ :type mode: `LinkMode`
+ :param subnet: The subnet to create the link on (optional).
+ :type subnet: `Subnet` or `int`
+ :param ip_address: The IP address to assign to the link.
+ :type ip_address: `str`
+ :param force: If True, allows `LinkMode.LINK_UP` to be created even if
+ other links already exist. Also allows the selection of any
+ subnet no matter the VLAN the subnet belongs to. Using this option
+ will cause all other interface links to be deleted (optional).
+ :type force: `bool`
+ :param default_gateway: If True, sets the gateway IP address for the
+ subnet as the default gateway for the node this interface belongs
+ to. Option can only be used with the `LinkMode.AUTO` and
+ `LinkMode.STATIC` modes.
+ :type default_gateway: `bool`
+
+ :returns: The created InterfaceLink.
+ :rtype: `InterfaceLink`
+ """
+ if not isinstance(interface, Interface):
+ raise TypeError(
+ "interface must be an Interface, not %s" % type(interface).__name__
+ )
+ if not isinstance(mode, LinkMode):
+ raise TypeError("mode must be a LinkMode, not %s" % type(mode).__name__)
+ if subnet is not None:
+ if isinstance(subnet, Subnet):
+ subnet = subnet.id
+ elif isinstance(subnet, int):
+ pass
+ else:
+ raise TypeError(
+ "subnet must be a Subnet or int, not %s" % type(subnet).__name__
+ )
+ if mode in [LinkMode.AUTO, LinkMode.STATIC]:
+ if subnet is None:
+ raise ValueError("subnet is required for %s" % mode)
+ if default_gateway and mode not in [LinkMode.AUTO, LinkMode.STATIC]:
+ raise ValueError("cannot set as default_gateway for %s" % mode)
+ params = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "mode": mode.value,
+ "force": force,
+ "default_gateway": default_gateway,
+ }
+ if subnet is not None:
+ params["subnet"] = subnet
+ if ip_address is not None:
+ params["ip_address"] = ip_address
+ # The API doesn't return just the link it returns the whole interface.
+ # Store the link ids before the save to find the addition at the end.
+ link_ids = {link.id for link in interface.links}
+ data = await interface._handler.link_subnet(**params)
+ # Update the links on the interface, except for the newly created link
+ # the `ManagedCreate` wrapper will add that to the interfaces link data
+ # automatically.
+ new_links = {link["id"]: link for link in data["links"]}
+ links_diff = list(set(new_links.keys()) - link_ids)
+ new_link = new_links.pop(links_diff[0])
+ interface._data["links"] = list(new_links.values())
+ interface._orig_data["links"] = copy.deepcopy(interface._data["links"])
+ return cls._object(new_link)
+
+
+class InterfaceLinks(ObjectSet, metaclass=InterfaceLinksType):
+ """A set of links on an `Interface`."""
+
+
+class InterfacesType(ObjectType):
+ """Metaclass for `Interfaces`."""
+
+ async def read(cls, node):
+ """Get list of `Interface`'s for `node`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ data = await cls._handler.read(system_id=system_id)
+ return cls(
+ cls._object(item, local_data={"node_system_id": system_id}) for item in data
+ )
+
+ async def create(
+ cls,
+ node: Union[Node, str],
+ interface_type: InterfaceType = InterfaceType.PHYSICAL,
+ *,
+ name: str = None,
+ mac_address: str = None,
+ tags: Iterable[str] = None,
+ vlan: Union[Vlan, int] = None,
+ parent: Union[Interface, int] = None,
+ parents: Iterable[Union[Interface, int]] = None,
+ mtu: int = None,
+ accept_ra: bool = None,
+ autoconf: bool = None,
+ bond_mode: str = None,
+ bond_miimon: int = None,
+ bond_downdelay: int = None,
+ bond_updelay: int = None,
+ bond_lacp_rate: str = None,
+ bond_xmit_hash_policy: str = None,
+ bridge_stp: bool = None,
+ bridge_fd: int = None
+ ):
+ """
+ Create a `Interface` in MAAS.
+
+ :param node: Node to create the interface on.
+ :type node: `Node` or `str`
+ :param interface_type: Type of interface to create (optional).
+ :type interface_type: `InterfaceType`
+ :param name: The name for the interface (optional).
+ :type name: `str`
+ :param tags: List of tags to add to the interface.
+ :type tags: sequence of `str`
+ :param mtu: The MTU for the interface (optional).
+ :type mtu: `int`
+ :param vlan: VLAN the interface is connected to (optional).
+ :type vlan: `Vlan` or `int`
+ :param accept_ra: True if the interface should accepted router
+ advertisements. (optional)
+ :type accept_ra: `bool`
+ :param autoconf: True if the interface should auto configure.
+ :type autoconf: `bool`
+
+ Following parameters specific to physical interface.
+
+ :param mac_address: The MAC address for the interface.
+ :type mac_address: `str`
+
+ Following parameters specific to a bond interface.
+
+ :param parents: Parent interfaces that make up the bond.
+ :type parents: sequence of `Interface` or `int`
+ :param mac_address: MAC address to use for the bond (optional).
+ :type mac_address: `str`
+ :param bond_mode: The operating mode of the bond (optional).
+ :type bond_mode: `str`
+ :param bond_miimon: The link monitoring freqeuncy in
+ milliseconds (optional).
+ :type bond_miimon: `int`
+ :param bond_downdelay: Specifies the time, in milliseconds, to wait
+ before disabling a slave after a link failure has been detected
+ (optional).
+ :type bond_downdelay: `int`
+ :param bond_updelay: Specifies the time, in milliseconds, to wait
+ before enabling a slave after a link recovery has been detected.
+ :type bond_updelay: `int`
+ :param bond_lacp_rate: Option specifying the rate in which we'll ask
+ our link partner to transmit LACPDU packets in 802.3ad
+ mode (optional).
+ :type bond_lacp_rate: `str`
+ :param bond_xmit_hash_policy: The transmit hash policy to use for
+ slave selection in balance-xor, 802.3ad, and tlb modes(optional).
+ :type bond_xmit_hash_policy: `str`
+
+ Following parameters specific to a VLAN interface.
+
+ :param parent: Parent interface for this VLAN interface.
+ :type parent: `Interface` or `int`
+
+ Following parameters specific to a Bridge interface.
+
+ :param parent: Parent interface for this bridge interface.
+ :type parent: `Interface` or `int`
+ :param mac_address: The MAC address for the interface (optional).
+ :type mac_address: `str`
+ :param bridge_stp: Turn spanning tree protocol on or off (optional).
+ :type bridge_stp: `bool`
+ :param bridge_fd: Set bridge forward delay to time seconds (optional).
+ :type bridge_fd: `int`
+
+ :returns: The created Interface.
+ :rtype: `Interface`
+ """
+ params = {}
+ if isinstance(node, str):
+ params["system_id"] = node
+ elif isinstance(node, Node):
+ params["system_id"] = node.system_id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % (type(node).__name__)
+ )
+
+ if name is not None:
+ params["name"] = name
+ if tags is not None:
+ params["tags"] = tags
+ if mtu is not None:
+ params["mtu"] = mtu
+ if vlan is not None:
+ if isinstance(vlan, Vlan):
+ params["vlan"] = vlan.id
+ elif isinstance(vlan, int):
+ params["vlan"] = vlan
+ else:
+ raise TypeError(
+ "vlan must be a Vlan or int, not %s" % (type(vlan).__name__)
+ )
+ if accept_ra is not None:
+ params["accept_ra"] = accept_ra
+ if autoconf is not None:
+ params["autoconf"] = autoconf
+
+ handler = None
+ if not isinstance(interface_type, InterfaceType):
+ raise TypeError(
+ "interface_type must be an InterfaceType, not %s"
+ % (type(interface_type).__name__)
+ )
+ if interface_type == InterfaceType.PHYSICAL:
+ handler = cls._handler.create_physical
+ if mac_address:
+ params["mac_address"] = mac_address
+ else:
+ raise ValueError("mac_address required for physical interface")
+ elif interface_type == InterfaceType.BOND:
+ handler = cls._handler.create_bond
+ if parent is not None:
+ raise ValueError("use parents not parent for bond interface")
+ if not isinstance(parents, Iterable):
+ raise TypeError(
+ "parents must be a iterable, not %s" % (type(parents).__name__)
+ )
+ if len(parents) == 0:
+ raise ValueError("at least one parent required for bond interface")
+ params["parents"] = list(gen_parents(parents))
+ if not name:
+ raise ValueError("name is required for bond interface")
+ if mac_address is not None:
+ params["mac_address"] = mac_address
+ if bond_mode is not None:
+ params["bond_mode"] = bond_mode
+ if bond_miimon is not None:
+ params["bond_miimon"] = bond_miimon
+ if bond_downdelay is not None:
+ params["bond_downdelay"] = bond_downdelay
+ if bond_updelay is not None:
+ params["bond_updelay"] = bond_updelay
+ if bond_lacp_rate is not None:
+ params["bond_lacp_rate"] = bond_lacp_rate
+ if bond_xmit_hash_policy is not None:
+ params["bond_xmit_hash_policy"] = bond_xmit_hash_policy
+ elif interface_type == InterfaceType.VLAN:
+ handler = cls._handler.create_vlan
+ if parents is not None:
+ raise ValueError("use parent not parents for VLAN interface")
+ if parent is None:
+ raise ValueError("parent is required for VLAN interface")
+ params["parent"] = get_parent(parent)
+ if vlan is None:
+ raise ValueError("vlan is required for VLAN interface")
+ elif interface_type == InterfaceType.BRIDGE:
+ handler = cls._handler.create_bridge
+ if parents is not None:
+ raise ValueError("use parent not parents for bridge interface")
+ if parent is None:
+ raise ValueError("parent is required for bridge interface")
+ params["parent"] = get_parent(parent)
+ if not name:
+ raise ValueError("name is required for bridge interface")
+ if mac_address is not None:
+ params["mac_address"] = mac_address
+ if bridge_stp is not None:
+ params["bridge_stp"] = bridge_stp
+ if bridge_fd is not None:
+ params["bridge_fd"] = bridge_fd
+ else:
+ raise ValueError("cannot create an interface of type: %s" % interface_type)
+
+ return cls._object(await handler(**params))
+
+
+class Interfaces(ObjectSet, metaclass=InterfacesType):
+ """The set of interfaces on a machine."""
+
+ @property
+ def by_name(self):
+ """Return mapping of name of interface to `Interface`."""
+ return {interface.name: interface for interface in self}
+
+ def get_by_name(self, name):
+ """Return an `Interface` by its name."""
+ return self.by_name[name]
diff --git a/maas/client/viscera/ip_addresses.py b/maas/client/viscera/ip_addresses.py
new file mode 100644
index 00000000..5997e76e
--- /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/ipranges.py b/maas/client/viscera/ipranges.py
new file mode 100644
index 00000000..3049eefa
--- /dev/null
+++ b/maas/client/viscera/ipranges.py
@@ -0,0 +1,90 @@
+"""Objects for ipranges."""
+
+__all__ = ["IPRanges", "IPRange"]
+
+from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType, to
+from .subnets import Subnet
+from ..enum import IPRangeType
+from typing import Union
+
+TYPE = type
+
+
+class IPRangesType(ObjectType):
+ """Metaclass for `IPRanges`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(
+ cls,
+ start_ip: str,
+ end_ip: str,
+ *,
+ type: IPRangeType = IPRangeType.RESERVED,
+ comment: str = None,
+ subnet: Union[Subnet, int] = None
+ ):
+ """
+ Create a `IPRange` in MAAS.
+
+ :param start_ip: First IP address in the range (required).
+ :type start_ip: `str`
+ :parma end_ip: Last IP address in the range (required).
+ :type end_ip: `str`
+ :param type: Type of IP address range (optional).
+ :type type: `IPRangeType`
+ :param comment: Reason for the IP address range (optional).
+ :type comment: `str`
+ :param subnet: Subnet the IP address range should be created on
+ (optional). By default MAAS will calculate the correct subnet
+ based on the `start_ip` and `end_ip`.
+ :type subnet: `Subnet` or `int`
+ :returns: The created IPRange
+ :rtype: `IPRange`
+ """
+ if not isinstance(type, IPRangeType):
+ raise TypeError("type must be an IPRangeType, not %s" % TYPE(type).__name__)
+
+ params = {"start_ip": start_ip, "end_ip": end_ip, "type": type.value}
+ if comment is not None:
+ params["comment"] = comment
+ if subnet is not None:
+ if isinstance(subnet, Subnet):
+ params["subnet"] = subnet.id
+ elif isinstance(subnet, int):
+ params["subnet"] = subnet
+ else:
+ raise TypeError(
+ "subnet must be Subnet or int, not %s" % (TYPE(subnet).__class__)
+ )
+ return cls._object(await cls._handler.create(**params))
+
+
+class IPRanges(ObjectSet, metaclass=IPRangesType):
+ """The set of IPRanges stored in MAAS."""
+
+
+class IPRangeTypeMeta(ObjectType):
+ """Metaclass for `IPRange`."""
+
+ async def read(cls, id: int):
+ """Get a `IPRange` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class IPRange(Object, metaclass=IPRangeTypeMeta):
+ """A IPRange."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ start_ip = ObjectField.Checked("start_ip", check(str))
+ end_ip = ObjectField.Checked("end_ip", check(str))
+ type = ObjectField.Checked("type", to(IPRangeType), readonly=True)
+ comment = ObjectField.Checked("comment", check(str))
+ subnet = ObjectFieldRelated("subnet", "Subnet", readonly=True, pk=0)
+
+ async def delete(self):
+ """Delete this IPRange."""
+ await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/logical_volumes.py b/maas/client/viscera/logical_volumes.py
new file mode 100644
index 00000000..d7416c2b
--- /dev/null
+++ b/maas/client/viscera/logical_volumes.py
@@ -0,0 +1,94 @@
+"""Objects for logical volumes."""
+
+__all__ = ["LogicalVolume", "LogicalVolumes"]
+
+from typing import Iterable
+
+from .block_devices import (
+ BlockDevice,
+ BlockDevices,
+ BlockDevicesType,
+ BlockDeviceTypeMeta,
+)
+from .volume_groups import VolumeGroup
+from ..utils import remove_None
+
+
+class LogicalVolumesType(BlockDevicesType):
+ """Metaclass for `LogicalVolumes`."""
+
+ def bind(cls, origin, handler, handlers, *, name=None):
+ # LogicalVolumes is just a wrapper over BlockDevices. So the
+ # `BlockDevices` handler is binded instead of an empty handler.
+ handler = handlers.get("BlockDevices")
+ return super(LogicalVolumesType, cls).bind(origin, handler, handlers)
+
+ async def create(
+ cls,
+ volume_group: VolumeGroup,
+ name: str,
+ size: int,
+ *,
+ uuid: str = None,
+ tags: Iterable[str] = None
+ ):
+ """
+ Create a logical volume on the volume group.
+
+ :param volume_group: Volume group to create the logical volume on.
+ :type node: `VolumeGroup`
+ :param name: The name for the logical volume.
+ :type name: `str`
+ :param size: The size of the logical volume in bytes.
+ :type size: `int`
+ :param uuid: UUID of the logical volume.
+ :type uuid: `str`
+ :param tags: List of tags to add to the logical volume.
+ :type tags: sequence of `str`
+ """
+ if not isinstance(volume_group, VolumeGroup):
+ raise TypeError(
+ "volume_group must be a VolumeGroup, not %s"
+ % (type(volume_group).__name__)
+ )
+
+ params = {"system_id": volume_group.node.system_id, "id": volume_group.id}
+ if not name:
+ raise ValueError("name must be provided.")
+ if not size or size < 0:
+ raise ValueError("size must be provided and greater than zero.")
+
+ params.update(remove_None({"name": name, "size": size, "uuid": uuid}))
+ data = await volume_group._handler.create_logical_volume(**params)
+ # Create logical volume doesn't return a full block device object.
+ # Load the logical volume using the block device endpoint, ensures that
+ # all the data present to access the fields.
+ bd_handler = getattr(cls._origin, "BlockDevice")._handler
+ volume = cls._object(
+ await bd_handler.read(system_id=data["system_id"], id=data["id"])
+ )
+ if tags:
+ volume.tags = tags
+ await volume.save()
+ return volume
+
+
+class LogicalVolumes(BlockDevices, metaclass=LogicalVolumesType):
+ """The set of logical volumes on a volume group."""
+
+
+class LogicalVolumeType(BlockDeviceTypeMeta):
+ """Metaclass for `LogicalVolume`."""
+
+ def bind(cls, origin, handler, handlers, *, name=None):
+ # LogicalVolume is just a wrapper over BlockDevice. So the
+ # `BlockDevice` handler is binded instead of an empty handler.
+ handler = handlers.get("BlockDevice")
+ return super(LogicalVolumeType, cls).bind(origin, handler, handlers)
+
+
+class LogicalVolume(BlockDevice, metaclass=LogicalVolumeType):
+ """A logical volume on a volume group."""
+
+ def __repr__(self):
+ return super(BlockDevice, self).__repr__(name="LogicalVolume", fields={"name"})
diff --git a/maas/client/viscera/maas.py b/maas/client/viscera/maas.py
index 20ee6ae7..e9604333 100644
--- a/maas/client/viscera/maas.py
+++ b/maas/client/viscera/maas.py
@@ -1,22 +1,31 @@
"""Objects for MAASs."""
-__all__ = [
- "MAAS",
-]
+__all__ = ["MAAS"]
import enum
import re
-from typing import (
- Optional,
- Sequence,
-)
-
-from . import (
- Object,
- ObjectType,
-)
+import typing
+
+from . import Object, ObjectType
from ..bones import CallError
-from ..utils.typecheck import typed
+
+
+def _django_boolean(boolean):
+ """Render a string suitable for use in a Django form.
+
+ Django's `BooleanField` understands "true", "false", "1", "0", "t", and
+ "f" as valid form values, but the widget `CheckboxInput` that is used by
+ `BooleanField` by default gets a shot at converting the form input first.
+ It inexplicably has different rules. See in `value_from_datadict`:
+
+ values = {'true': True, 'false': False}
+ if isinstance(value, six.string_types):
+ value = values.get(value.lower(), value)
+ return bool(value)
+
+ Sigh.
+ """
+ return "true" if boolean else "false"
class DescriptiveEnum(enum.Enum):
@@ -36,25 +45,20 @@ def lookup(cls, parameter):
if member.parameter == parameter:
return member
else:
- raise KeyError(
- "%s value %r not recognised."
- % (cls.__name__, parameter))
+ raise KeyError("%s value %r not recognised." % (cls.__name__, parameter))
class MAASType(ObjectType):
"""Metaclass for `MAAS`."""
- @typed
async def get_name(cls) -> str:
"""The name of the MAAS instance."""
return await cls.get_config("maas_name")
- @typed
async def set_name(cls, name: str):
"""See `get_name`."""
return await cls.set_config("maas_name", name)
- @typed
async def get_main_archive(cls) -> str:
"""Main archive URL.
@@ -63,12 +67,10 @@ async def get_main_archive(cls) -> str:
"""
return await cls.get_config("main_archive")
- @typed
async def set_main_archive(cls, url: str):
"""See `get_main_archive`."""
await cls.set_config("main_archive", url)
- @typed
async def get_ports_archive(cls) -> str:
"""Ports archive.
@@ -77,58 +79,48 @@ async def get_ports_archive(cls) -> str:
"""
return await cls.get_config("ports_archive")
- @typed
async def set_ports_archive(cls, series: str):
"""See `get_ports_archive`."""
await cls.set_config("ports_archive", series)
- @typed
async def get_default_os(cls) -> str:
"""Default OS used for deployment."""
return await cls.get_config("default_osystem")
- @typed
async def set_default_os(cls, series: str):
"""See `get_default_os`."""
await cls.set_config("default_osystem", series)
- @typed
async def get_default_distro_series(cls) -> str:
"""Default OS release used for deployment."""
return await cls.get_config("default_distro_series")
- @typed
async def set_default_distro_series(cls, series: str):
"""See `get_default_distro_series`."""
await cls.set_config("default_distro_series", series)
- @typed
async def get_commissioning_distro_series(cls) -> str:
"""Default Ubuntu release used for commissioning."""
return await cls.get_config("commissioning_distro_series")
- @typed
async def set_commissioning_distro_series(cls, series: str):
"""See `get_commissioning_distro_series`."""
await cls.set_config("commissioning_distro_series", series)
- @typed
- async def get_http_proxy(cls) -> Optional[str]:
+ async def get_http_proxy(cls) -> typing.Optional[str]:
"""Proxy for APT and HTTP/HTTPS.
This will be passed onto provisioned nodes to use as a proxy for APT
traffic. MAAS also uses the proxy for downloading boot images. If no
URL is provided, the built-in MAAS proxy will be used.
"""
- data = cls.get_config("http_proxy")
+ data = await cls.get_config("http_proxy")
return None if data is None or data == "" else data
- @typed
- async def set_http_proxy(cls, url: Optional[str]):
+ async def set_http_proxy(cls, url: typing.Optional[str]):
"""See `get_http_proxy`."""
await cls.set_config("http_proxy", "" if url is None else url)
- @typed
async def get_enable_http_proxy(cls) -> bool:
"""Enable the use of an APT and HTTP/HTTPS proxy.
@@ -137,12 +129,10 @@ async def get_enable_http_proxy(cls) -> bool:
"""
return await cls.get_config("enable_http_proxy")
- @typed
async def set_enable_http_proxy(cls, enabled: bool):
"""See `get_enable_http_proxy`."""
- await cls.set_config("enable_http_proxy", "1" if enabled else "0")
+ await cls.set_config("enable_http_proxy", _django_boolean(enabled))
- @typed
async def get_curtin_verbose(cls) -> bool:
"""Should `curtin` log with high verbosity?
@@ -151,26 +141,22 @@ async def get_curtin_verbose(cls) -> bool:
"""
return await cls.get_config("curtin_verbose")
- @typed
async def set_curtin_verbose(cls, verbose: bool):
"""See `get_curtin_verbose`."""
- await cls.set_config("curtin_verbose", "1" if verbose else "0")
+ await cls.set_config("curtin_verbose", _django_boolean(verbose))
- @typed
- async def get_kernel_options(cls) -> Optional[str]:
+ async def get_kernel_options(cls) -> typing.Optional[str]:
"""Kernel options.
Boot parameters to pass to the kernel by default.
"""
- data = cls.get_config("kernel_opts")
+ data = await cls.get_config("kernel_opts")
return None if data is None or data == "" else data
- @typed
- async def set_kernel_options(cls, options: Optional[str]):
+ async def set_kernel_options(cls, options: typing.Optional[str]):
"""See `get_kernel_options`."""
await cls.set_config("kernel_opts", "" if options is None else options)
- @typed
async def get_upstream_dns(cls) -> list:
"""Upstream DNS server addresses.
@@ -179,14 +165,14 @@ async def get_upstream_dns(cls) -> list:
DNS server. This value is used as the value of 'forwarders' in the DNS
server config.
"""
- data = cls.get_config("upstream_dns")
- return [] if data is None else re.split("[,\s]+", data)
+ data = await cls.get_config("upstream_dns")
+ return [] if data is None else re.split(r"[,\s]+", data)
- @typed
- async def set_upstream_dns(cls, addresses: Optional[Sequence[str]]):
+ async def set_upstream_dns(cls, addresses: typing.Optional[typing.Sequence[str]]):
"""See `get_upstream_dns`."""
- await cls.set_config("upstream_dns", (
- "" if addresses is None else",".join(addresses)))
+ await cls.set_config(
+ "upstream_dns", ("" if addresses is None else ",".join(addresses))
+ )
class DNSSEC(DescriptiveEnum):
"""DNSSEC validation settings.
@@ -198,73 +184,61 @@ class DNSSEC(DescriptiveEnum):
YES = "yes", "Yes (manually configured root key)"
NO = "no", "No (disable DNSSEC)"
- @typed
async def get_dnssec_validation(cls) -> DNSSEC:
"""Enable DNSSEC validation of upstream zones.
Only used when MAAS is running its own DNS server. This value is used
as the value of 'dnssec_validation' in the DNS server config.
"""
- data = cls.get_config("dnssec_validation")
+ data = await cls.get_config("dnssec_validation")
return cls.DNSSEC.lookup(data)
- @typed
async def set_dnssec_validation(cls, validation: DNSSEC):
"""See `get_dnssec_validation`."""
await cls.set_config("dnssec_validation", validation.parameter)
- @typed
async def get_default_dns_ttl(cls) -> int:
"""Default Time-To-Live for DNS records.
If no TTL value is specified at a more specific point this is how long
DNS responses are valid, in seconds.
"""
- return int(cls.get_config("default_dns_ttl"))
+ return int(await cls.get_config("default_dns_ttl"))
- @typed
async def set_default_dns_ttl(cls, ttl: int):
"""See `get_default_dns_ttl`."""
await cls.set_config("default_dns_ttl", str(ttl))
- @typed
async def get_enable_disk_erasing_on_release(cls) -> bool:
"""Should nodes' disks be erased prior to releasing."""
return await cls.get_config("enable_disk_erasing_on_release")
- @typed
async def set_enable_disk_erasing_on_release(cls, erase: bool):
"""Should nodes' disks be erased prior to releasing."""
- await cls.set_config(
- "enable_disk_erasing_on_release", "1" if erase else "0")
+ await cls.set_config("enable_disk_erasing_on_release", _django_boolean(erase))
- @typed
- async def get_windows_kms_host(cls) -> Optional[str]:
+ async def get_windows_kms_host(cls) -> typing.Optional[str]:
"""Windows KMS activation host.
FQDN or IP address of the host that provides the KMS Windows
activation service. (Only needed for Windows deployments using KMS
activation.)
"""
- data = cls.get_config("windows_kms_host")
+ data = await cls.get_config("windows_kms_host")
return None if data is None or data == "" else data
- @typed
- async def set_windows_kms_host(cls, host: Optional[str]):
+ async def set_windows_kms_host(cls, host: typing.Optional[str]):
"""See `get_windows_kms_host`."""
await cls.set_config("windows_kms_host", "" if host is None else host)
- @typed
async def get_boot_images_auto_import(cls) -> bool:
"""Automatically import/refresh the boot images every 60 minutes."""
return await cls.get_config("boot_images_auto_import")
- @typed
async def set_boot_images_auto_import(cls, auto: bool):
"""See `get_boot_images_auto_import`."""
- await cls.set_config("boot_images_auto_import", "1" if auto else "0")
+ await cls.set_config("boot_images_auto_import", _django_boolean(auto))
- @typed
async def get_ntp_server(cls) -> str:
"""Address of NTP server.
@@ -273,7 +247,6 @@ async def get_ntp_server(cls) -> str:
"""
return await cls.get_config("ntp_server")
- @typed
async def set_ntp_server(cls, server: str):
"""See `get_ntp_server`."""
await cls.set_config("ntp_server", server)
@@ -284,45 +257,39 @@ class StorageLayout(DescriptiveEnum):
LVM = "lvm", "LVM layout"
BCACHE = "bcache", "Bcache layout"
- @typed
async def get_default_storage_layout(cls) -> StorageLayout:
"""Default storage layout.
Storage layout that is applied to a node when it is deployed.
"""
- data = cls.get_config("default_storage_layout")
+ data = await cls.get_config("default_storage_layout")
return cls.StorageLayout.lookup(data)
- @typed
async def set_default_storage_layout(cls, series: StorageLayout):
"""See `get_default_storage_layout`."""
await cls.set_config("default_storage_layout", series.parameter)
- @typed
- async def get_default_min_hwe_kernel(cls) -> Optional[str]:
+ async def get_default_min_hwe_kernel(cls) -> typing.Optional[str]:
"""Default minimum kernel version.
The minimum kernel version used on new and commissioned nodes.
"""
- data = cls.get_config("default_min_hwe_kernel")
+ data = await cls.get_config("default_min_hwe_kernel")
return None if data is None or data == "" else data
- @typed
- async def set_default_min_hwe_kernel(cls, version: Optional[str]):
+ async def set_default_min_hwe_kernel(cls, version: typing.Optional[str]):
"""See `get_default_min_hwe_kernel`."""
await cls.set_config(
- "default_min_hwe_kernel", "" if version is None else version)
+ "default_min_hwe_kernel", "" if version is None else version
+ )
- @typed
async def get_enable_third_party_drivers(cls) -> bool:
"""Enable the installation of proprietary drivers, e.g. HPVSA."""
return await cls.get_config("enable_third_party_drivers")
- @typed
async def set_enable_third_party_drivers(cls, enabled: bool):
"""See `get_enable_third_party_drivers`."""
- await cls.set_config(
- "enable_third_party_drivers", "1" if enabled else "0")
+ await cls.set_config("enable_third_party_drivers", _django_boolean(enabled))
async def get_config(cls, name: str):
"""Get a configuration value from MAAS.
@@ -343,11 +310,13 @@ async def set_config(cls, name: str, value):
async def _roundtrip(cls):
"""Testing helper: gets each value and sets it again."""
getters = {
- name[4:]: getattr(cls, name) for name in dir(cls)
+ name[4:]: getattr(cls, name)
+ for name in dir(cls)
if name.startswith("get_") and name != "get_config"
}
setters = {
- name[4:]: getattr(cls, name) for name in dir(cls)
+ name[4:]: getattr(cls, name)
+ for name in dir(cls)
if name.startswith("set_") and name != "set_config"
}
@@ -363,23 +332,9 @@ async def _roundtrip(cls):
print(error)
print(error.content.decode("utf-8", "replace"))
else:
- value2 = getter()
+ value2 = await getter()
if value2 != value:
- print(
- "!!! Round-trip failed:", repr(value),
- "-->", repr(value2))
-
- getters_without_setters = set(getters).difference(setters)
- if getters_without_setters:
- print(
- "!!! Getters without setters:",
- " ".join(getters_without_setters))
-
- setters_without_getters = set(setters).difference(getters)
- if setters_without_getters:
- print(
- "!!! Setters without getters:",
- " ".join(setters_without_getters))
+ print("!!! Round-trip failed:", repr(value), "-->", repr(value2))
class MAAS(Object, metaclass=MAASType):
diff --git a/maas/client/viscera/machines.py b/maas/client/viscera/machines.py
index 6c37c885..f9ac96dc 100644
--- a/maas/client/viscera/machines.py
+++ b/maas/client/viscera/machines.py
@@ -1,64 +1,271 @@
"""Objects for machines."""
-__all__ = [
- "Machine",
- "Machines",
-]
+__all__ = ["Machine", "Machines"]
+import asyncio
import base64
+import bson
+import json
from http import HTTPStatus
-from typing import (
- List,
- Sequence,
- Union,
-)
+import typing
from . import (
check,
check_optional,
- Object,
ObjectField,
- ObjectSet,
- ObjectType,
- zones,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ to,
)
+from .fabrics import Fabric
+from .interfaces import Interface
+from .nodes import Node, Nodes, NodesType, NodeTypeMeta
+from .pods import Pod
+from .subnets import Subnet
+from .zones import Zone
from ..bones import CallError
+from ..enum import NodeStatus, PowerState, PowerStopMode
+from ..errors import MAASException, OperationNotAllowed, PowerError
+from ..utils import remove_None
+from ..utils.diff import calculate_dict_diff
+
+
+FabricParam = typing.Union[str, int, Fabric]
+InterfaceParam = typing.Union[str, int, Interface]
+SubnetParam = typing.Union[str, int, Subnet]
+ZoneParam = typing.Union[str, Zone]
+
+def get_param_arg(param, idx, klass, arg, attr="id"):
+ """Return the correct value for a fabric from `arg`."""
+ if isinstance(arg, klass):
+ return getattr(arg, attr)
+ elif isinstance(arg, (int, str)):
+ return arg
+ else:
+ raise TypeError(
+ "%s[%d] must be int, str, or %s, not %s"
+ % (param, idx, klass.__name__, type(arg).__name__)
+ )
-class MachinesType(ObjectType):
+
+class MachinesType(NodesType):
"""Metaclass for `Machines`."""
- async def read(cls):
- data = await cls._handler.read()
- return cls(map(cls._object, data))
+ async def create(
+ cls,
+ architecture: str,
+ mac_addresses: typing.Sequence[str],
+ power_type: str,
+ power_parameters: typing.Mapping[str, typing.Any] = None,
+ *,
+ subarchitecture: str = None,
+ min_hwe_kernel: str = None,
+ hostname: str = None,
+ domain: typing.Union[int, str] = None
+ ):
+ """
+ Create a new machine.
+
+ :param architecture: The architecture of the machine (required).
+ :type architecture: `str`
+ :param mac_addresses: The MAC address of the machine (required).
+ :type mac_addresses: sequence of `str`
+ :param power_type: The power type of the machine (required).
+ :type power_type: `str`
+ :param power_parameters: The power parameters for the machine
+ (optional).
+ :type power_parameters: mapping of `str` to any value.
+ :param subarchitecture: The subarchitecture of the machine (optional).
+ :type subarchitecture: `str`
+ :param min_hwe_kernel: The minimal HWE kernel for the machine
+ (optional).
+ :param hostname: The hostname for the machine (optional).
+ :type hostname: `str`
+ :param domain: The domain for the machine (optional).
+ :type domain: `int` or `str`
+ """
+ params = {
+ "architecture": architecture,
+ "mac_addresses": mac_addresses,
+ "power_type": power_type,
+ }
+ if power_parameters is not None:
+ params["power_parameters"] = json.dumps(power_parameters, sort_keys=True)
+ if subarchitecture is not None:
+ params["subarchitecture"] = subarchitecture
+ if min_hwe_kernel is not None:
+ params["min_hwe_kernel"] = min_hwe_kernel
+ if hostname is not None:
+ params["hostname"] = hostname
+ if domain is not None:
+ params["domain"] = domain
+ return cls._object(await cls._handler.create(**params))
async def allocate(
- cls, *, hostname: str=None, architecture: str=None,
- cpus: int=None, memory: float=None, tags: Sequence[str]=None):
+ cls,
+ *,
+ hostname: str = None,
+ architectures: typing.Sequence[str] = None,
+ cpus: int = None,
+ fabrics: typing.Sequence[FabricParam] = None,
+ interfaces: typing.Sequence[InterfaceParam] = None,
+ memory: float = None,
+ pod: typing.Union[str, Pod] = None,
+ not_pod: typing.Union[str, Pod] = None,
+ pod_type: str = None,
+ not_pod_type: str = None,
+ storage: typing.Sequence[str] = None,
+ subnets: typing.Sequence[SubnetParam] = None,
+ tags: typing.Sequence[str] = None,
+ zone: typing.Union[str, Zone] = None,
+ not_fabrics: typing.Sequence[FabricParam] = None,
+ not_subnets: typing.Sequence[SubnetParam] = None,
+ not_tags: typing.Sequence[str] = None,
+ not_zones: typing.Sequence[ZoneParam] = None,
+ agent_name: str = None,
+ comment: str = None,
+ bridge_all: bool = None,
+ bridge_stp: bool = None,
+ bridge_fd: int = None,
+ dry_run: bool = None,
+ verbose: bool = None
+ ):
"""
+ Allocate a machine.
+
:param hostname: The hostname to match.
- :param architecture: The architecture to match, e.g. "amd64".
+ :type hostname: `str`
+ :param architectures: The architecture(s) to match.
+ :type architectures: sequence of `str`
:param cpus: The minimum number of CPUs to match.
- :param memory: The minimum amount of RAM to match.
- :param tags: The tags to match, as a sequence. Each tag may be
- prefixed with a hyphen to denote that the given tag should NOT be
- associated with a matched machine.
+ :type cpus: `int`
+ :param fabrics: The connected fabrics to match.
+ :type fabrics: sequence of either `str`, `int`, or `Fabric`
+ :param interfaces: The interfaces to match.
+ :type interfaces: sequence of either `str`, `int`, or `Interface`
+ :param memory: The minimum amount of RAM to match in MiB.
+ :type memory: `int`
+ :param pod: The pod to allocate the machine from.
+ :type pod: `str`
+ :param not_pod: Pod the machine must not be located in.
+ :type not_pod: `str`
+ :param pod_type: The type of pod to allocate the machine from.
+ :type pod_type: `str`
+ :param not_pod_type: Pod type the machine must not be located in.
+ :type not_pod_type: `str`
+ :param subnets: The subnet(s) the desired machine must be linked to.
+ :type subnets: sequence of `str` or `int` or `Subnet`
+ :param storage: The storage contraint to match.
+ :type storage: `str`
+ :param tags: The tags to match, as a sequence.
+ :type tags: sequence of `str`
+ :param zone: The zone the desired machine must belong to.
+ :type zone: `str` or `Zone`
+ :param not_fabrics: The fabrics the machine must NOT be connected to.
+ :type not_fabrics: sequence of either `str`, `int`, or `Fabric`
+ :param not_subnets: The subnet(s) the desired machine must NOT be
+ linked to.
+ :type not_subnets: sequence of `str` or `int` or `Subnet`
+ :param not_zones: The zone(s) the desired machine must NOT in.
+ :type not_zones: sequence of `str` or `Zone`
+ :param agent_name: Agent name to attach to the acquire machine.
+ :type agent_name: `str`
+ :param comment: Comment for the allocate event placed on machine.
+ :type comment: `str`
+ :param bridge_all: Automatically create a bridge on all interfaces
+ on the allocated machine.
+ :type bridge_all: `bool`
+ :param bridge_stp: Turn spaning tree protocol on or off for the
+ bridges created with bridge_all.
+ :type bridge_stp: `bool`
+ :param bridge_fd: Set the forward delay in seconds on the bridges
+ created with bridge_all.
+ :type bridge_fd: `int`
+ :param dry_run: Don't actually acquire the machine just return the
+ machine that would have been acquired.
+ :type dry_run: `bool`
+ :param verbose: Indicate that the user would like additional verbosity
+ in the constraints_by_type field (each constraint will be prefixed
+ by `verbose_`, and contain the full data structure that indicates
+ which machine(s) matched).
+ :type verbose: `bool`
"""
- params = {}
- if hostname is not None:
- params["name"] = hostname
- if architecture is not None:
- params["architecture"] = architecture
- if cpus is not None:
- params["cpu_count"] = str(cpus)
- if memory is not None:
- params["mem"] = str(memory)
- if tags is not None:
- params["tags"] = [
- tag for tag in tags if not tag.startswith("-")]
- params["not_tags"] = [
- tag[1:] for tag in tags if tag.startswith("-")]
-
+ params = remove_None(
+ {
+ "name": hostname,
+ "arch": architectures,
+ "cpu_count": str(cpus) if cpus else None,
+ "mem": str(memory) if memory else None,
+ "pod_type": pod_type,
+ "not_pod_type": not_pod_type,
+ "storage": storage,
+ "tags": tags,
+ "not_tags": not_tags,
+ "agent_name": agent_name,
+ "comment": comment,
+ "bridge_all": bridge_all,
+ "bridge_stp": bridge_stp,
+ "bridge_fd": bridge_fd,
+ "dry_run": dry_run,
+ "verbose": verbose,
+ }
+ )
+ if fabrics is not None:
+ params["fabrics"] = [
+ get_param_arg("fabrics", idx, Fabric, fabric)
+ for idx, fabric in enumerate(fabrics)
+ ]
+ if interfaces is not None:
+ params["interfaces"] = [
+ get_param_arg("interfaces", idx, Interface, nic)
+ for idx, nic in enumerate(interfaces)
+ ]
+ if pod is not None:
+ if isinstance(pod, Pod):
+ params["pod"] = pod.name
+ elif isinstance(pod, str):
+ params["pod"] = pod
+ else:
+ raise TypeError("pod must be a str or Pod, not %s" % type(pod).__name__)
+ if not_pod is not None:
+ if isinstance(not_pod, Pod):
+ params["not_pod"] = not_pod.name
+ elif isinstance(not_pod, str):
+ params["not_pod"] = not_pod
+ else:
+ raise TypeError(
+ "not_pod must be a str or Pod, not %s" % type(not_pod).__name__
+ )
+ if subnets is not None:
+ params["subnets"] = [
+ get_param_arg("subnets", idx, Subnet, subnet)
+ for idx, subnet in enumerate(subnets)
+ ]
+ if zone is not None:
+ if isinstance(zone, Zone):
+ params["zone"] = zone.name
+ elif isinstance(zone, str):
+ params["zone"] = zone
+ else:
+ raise TypeError(
+ "zone must be a str or Zone, not %s" % type(zone).__name__
+ )
+ if not_fabrics is not None:
+ params["not_fabrics"] = [
+ get_param_arg("not_fabrics", idx, Fabric, fabric)
+ for idx, fabric in enumerate(not_fabrics)
+ ]
+ if not_subnets is not None:
+ params["not_subnets"] = [
+ get_param_arg("not_subnets", idx, Subnet, subnet)
+ for idx, subnet in enumerate(not_subnets)
+ ]
+ if not_zones is not None:
+ params["not_in_zones"] = [
+ get_param_arg("not_zones", idx, Zone, zone, attr="name")
+ for idx, zone in enumerate(not_zones)
+ ]
try:
data = await cls._handler.allocate(**params)
except CallError as error:
@@ -70,89 +277,213 @@ async def allocate(
else:
return cls._object(data)
+ async def get_power_parameters_for(cls, system_ids: typing.Sequence[str]):
+ """
+ Get a list of power parameters for specified systems.
+ *WARNING*: This method is considered 'alpha' and may be modified
+ in future.
+
+ :param system_ids: The system IDs to get power parameters for
+ """
+ if len(system_ids) == 0:
+ return {}
+ data = await cls._handler.power_parameters(id=system_ids)
+ return data
+
class MachineNotFound(Exception):
- """Machine was not found."""
+ """
+ Machine was not found.
+
+ Not a MAASException because this doesn't occur in the context of
+ a specific object.
+ """
+
+
+class RescueModeFailure(MAASException):
+ """Machine failed to perform a Rescue mode transition."""
+
+
+class FailedCommissioning(MAASException):
+ """Machine failed to commission."""
+
+
+class FailedTesting(MAASException):
+ """Machine failed testing."""
+
+
+class FailedDeployment(MAASException):
+ """Machine failed to deploy."""
+
+
+class FailedReleasing(MAASException):
+ """Machine failed to release."""
+
+
+class FailedDiskErasing(MAASException):
+ """Machine failed to erase disk when releasing."""
-class Machines(ObjectSet, metaclass=MachinesType):
+class Machines(Nodes, metaclass=MachinesType):
"""The set of machines stored in MAAS."""
-class MachineType(ObjectType):
+class MachineType(NodeTypeMeta):
+ """Metaclass for `Machine`."""
async def read(cls, system_id):
data = await cls._handler.read(system_id=system_id)
return cls(data)
-class Machine(Object, metaclass=MachineType):
+class Machine(Node, metaclass=MachineType):
"""A machine stored in MAAS."""
architecture = ObjectField.Checked(
- "architecture", check_optional(str), check_optional(str))
- boot_disk = ObjectField.Checked(
- "boot_disk", check_optional(str), check_optional(str))
- cpus = ObjectField.Checked(
- "cpu_count", check(int), check(int))
- disable_ipv4 = ObjectField.Checked(
- "disable_ipv4", check(bool), check(bool))
- distro_series = ObjectField.Checked(
- "distro_series", check(str), check(str))
- hostname = ObjectField.Checked(
- "hostname", check(str), check(str))
+ "architecture", check_optional(str), check_optional(str)
+ )
+ boot_disk = ObjectFieldRelated("boot_disk", "BlockDevice", readonly=True)
+ boot_interface = ObjectFieldRelated("boot_interface", "Interface", readonly=True)
+ block_devices = ObjectFieldRelatedSet(
+ "blockdevice_set", "BlockDevices", reverse=None
+ )
+ bcaches = ObjectFieldRelatedSet("bcaches", "Bcaches", reverse=None)
+ cache_sets = ObjectFieldRelatedSet("cache_sets", "BcacheCacheSets", reverse=None)
+ cpus = ObjectField.Checked("cpu_count", check(int), check(int))
+ disable_ipv4 = ObjectField.Checked("disable_ipv4", check(bool), check(bool))
+ distro_series = ObjectField.Checked("distro_series", check(str), readonly=True)
hwe_kernel = ObjectField.Checked(
- "hwe_kernel", check_optional(str), check_optional(str))
- ip_addresses = ObjectField.Checked(
- "ip_addresses", check(List[str]), readonly=True)
- memory = ObjectField.Checked(
- "memory", check(int), check(int))
+ "hwe_kernel", check_optional(str), check_optional(str)
+ )
+ locked = ObjectField.Checked("locked", check(bool), readonly=True)
+ memory = ObjectField.Checked("memory", check(int), check(int))
min_hwe_kernel = ObjectField.Checked(
- "min_hwe_kernel", check_optional(str), check_optional(str))
-
- # blockdevice_set
- # interface_set
- # macaddress_set
- # netboot
- # osystem
- # owner
- # physicalblockdevice_set
-
- # TODO: Use an enum here.
- power_state = ObjectField.Checked(
- "power_state", check(str), readonly=True)
-
- # power_type
- # pxe_mac
- # resource_uri
- # routers
- # status
- # storage
-
- status = ObjectField.Checked(
- "status", check(int), readonly=True)
+ "min_hwe_kernel", check_optional(str), check_optional(str)
+ )
+ netboot = ObjectField.Checked("netboot", check(bool), readonly=True)
+ osystem = ObjectField.Checked("osystem", check(str), readonly=True)
+ owner_data = ObjectField.Checked("owner_data", check(dict), check(dict))
+ status = ObjectField.Checked("status", to(NodeStatus), readonly=True)
status_action = ObjectField.Checked(
- "status_action", check_optional(str), readonly=True)
+ "status_action", check_optional(str), readonly=True
+ )
status_message = ObjectField.Checked(
- "status_message", check_optional(str), readonly=True)
- status_name = ObjectField.Checked(
- "status_name", check(str), readonly=True)
+ "status_message", check_optional(str), readonly=True
+ )
+ 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)
- # swap_size
+ async def save(self):
+ """Save the machine in MAAS."""
+ orig_owner_data = self._orig_data["owner_data"]
+ new_owner_data = dict(self._data["owner_data"])
+ self._changed_data.pop("owner_data", None)
+ await super(Machine, self).save()
+ params_diff = calculate_dict_diff(orig_owner_data, new_owner_data)
+ if len(params_diff) > 0:
+ params_diff["system_id"] = self.system_id
+ await self._handler.set_owner_data(**params_diff)
+ self._data["owner_data"] = self._data["owner_data"]
- system_id = ObjectField.Checked(
- "system_id", check(str), readonly=True)
- tags = ObjectField.Checked(
- "tag_names", check(List[str]), readonly=True)
+ async def abort(self, *, comment: str = None):
+ """Abort the current action.
- # virtualblockdevice_set
+ :param comment: Reason for aborting the action.
+ :param type: `str`
+ """
+ params = {"system_id": self.system_id}
+ if comment:
+ params["comment"] = comment
+ self._reset(await self._handler.abort(**params))
+ return self
+
+ async def clear_default_gateways(self):
+ """Clear default gateways."""
+ self._reset(
+ await self._handler.clear_default_gateways(system_id=self.system_id)
+ )
+ return self
- zone = zones.ZoneField(
- "zone", readonly=True)
+ async def commission(
+ self,
+ *,
+ enable_ssh: bool = None,
+ skip_networking: bool = None,
+ skip_storage: bool = None,
+ commissioning_scripts: typing.Sequence[str] = None,
+ testing_scripts: typing.Sequence[str] = None,
+ wait: bool = False,
+ wait_interval: int = 5
+ ):
+ """Commission this machine.
+
+ :param enable_ssh: Prevent the machine from powering off after running
+ commissioning scripts and enable your user to SSH into the machine.
+ :type enable_ssh: `bool`
+ :param skip_networking: Skip updating the MAAS interfaces for the
+ machine.
+ :type skip_networking: `bool`
+ :param skip_storage: Skip update the MAAS block devices for the
+ machine.
+ :type skip_storage: `bool`
+ :param commissioning_scripts: List of extra commisisoning scripts
+ to run. If the name of the commissioning scripts match a tag, then
+ all commissioning scripts with that tag will be used.
+ :type commissioning_scripts: sequence of `str`
+ :param testing_scripts: List of testing scripts to run after
+ commissioning. By default a small set of testing scripts will run
+ by default. Passing empty list will disable running any testing
+ scripts during commissioning. If the name of the testing scripts
+ match a tag, then all testing scripts with that tag will be used.
+ :type testing_scripts: sequence of `str`
+ :param wait: If specified, wait until the commissioning is complete.
+ :param wait_interval: How often to poll, defaults to 5 seconds
+ """
+ params = {"system_id": self.system_id}
+ if enable_ssh is not None:
+ params["enable_ssh"] = enable_ssh
+ if skip_networking is not None:
+ params["skip_networking"] = skip_networking
+ if skip_storage is not None:
+ params["skip_storage"] = skip_storage
+ if commissioning_scripts is not None and len(commissioning_scripts) > 0:
+ params["commissioning_scripts"] = ",".join(commissioning_scripts)
+ if testing_scripts is not None:
+ if len(testing_scripts) == 0 or testing_scripts == "none":
+ params["testing_scripts"] = ["none"]
+ else:
+ params["testing_scripts"] = ",".join(testing_scripts)
+ self._reset(await self._handler.commission(**params))
+ if not wait:
+ return self
+ else:
+ # Wait for the machine to be fully commissioned.
+ while self.status in [NodeStatus.COMMISSIONING, NodeStatus.TESTING]:
+ await asyncio.sleep(wait_interval)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.status == NodeStatus.FAILED_COMMISSIONING:
+ msg = "{hostname} failed to commission.".format(hostname=self.hostname)
+ raise FailedCommissioning(msg, self)
+ if self.status == NodeStatus.FAILED_TESTING:
+ msg = "{hostname} failed testing.".format(hostname=self.hostname)
+ raise FailedTesting(msg, self)
+ return self
async def deploy(
- self, user_data: Union[bytes, str]=None, distro_series: str=None,
- hwe_kernel: str=None, comment: str=None):
+ self,
+ *,
+ user_data: typing.Union[bytes, str] = None,
+ distro_series: str = None,
+ hwe_kernel: str = None,
+ comment: str = None,
+ wait: bool = False,
+ install_kvm: bool = False,
+ wait_interval: int = 5,
+ ephemeral_deploy: bool = False,
+ enable_hw_sync: bool = False
+ ):
"""Deploy this machine.
:param user_data: User-data to provide to the machine when booting. If
@@ -163,8 +494,16 @@ async def deploy(
:param hwe_kernel: The HWE kernel to deploy. Probably only relevant
when deploying Ubuntu.
: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}
+
+ if install_kvm:
+ params["install_kvm"] = install_kvm
+
if user_data is not None:
if isinstance(user_data, bytes):
params["user_data"] = base64.encodebytes(user_data)
@@ -178,16 +517,325 @@ async def deploy(
params["hwe_kernel"] = hwe_kernel
if comment is not None:
params["comment"] = comment
- data = await self._handler.deploy(**params)
- return type(self)(data)
+ 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
+ else:
+ # Wait for the machine to be fully deployed
+ while self.status == NodeStatus.DEPLOYING:
+ await asyncio.sleep(wait_interval)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.status == NodeStatus.FAILED_DEPLOYMENT:
+ msg = "{hostname} failed to deploy.".format(hostname=self.hostname)
+ raise FailedDeployment(msg, self)
+ return self
+
+ async def enter_rescue_mode(self, wait: bool = False, wait_interval: int = 5):
+ """
+ Send this machine into 'rescue mode'.
+
+ :param wait: If specified, wait until the deploy is complete.
+ :param wait_interval: How often to poll, defaults to 5 seconds
+ """
+ try:
+ self._reset(await self._handler.rescue_mode(system_id=self.system_id))
+ except CallError as error:
+ if error.status == HTTPStatus.FORBIDDEN:
+ message = "Not allowed to enter rescue mode"
+ raise OperationNotAllowed(message) from error
+ else:
+ raise
+
+ if not wait:
+ return self
+ else:
+ # Wait for machine to finish entering rescue mode
+ while self.status == NodeStatus.ENTERING_RESCUE_MODE:
+ await asyncio.sleep(wait)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.status == NodeStatus.FAILED_ENTERING_RESCUE_MODE:
+ msg = "{hostname} failed to enter rescue mode.".format(
+ hostname=self.hostname
+ )
+ raise RescueModeFailure(msg, self)
+ return self
+
+ async def exit_rescue_mode(self, wait: bool = False, wait_interval: int = 5):
+ """
+ Exit rescue mode.
+
+ :param wait: If specified, wait until the deploy is complete.
+ :param wait_interval: How often to poll, defaults to 5 seconds
+ """
+ try:
+ self._reset(await self._handler.exit_rescue_mode(system_id=self.system_id))
+ except CallError as error:
+ if error.status == HTTPStatus.FORBIDDEN:
+ message = "Not allowed to exit rescue mode."
+ raise OperationNotAllowed(message) from error
+ else:
+ raise
+ if not wait:
+ return self
+ else:
+ # Wait for machine to finish exiting rescue mode
+ while self.status == NodeStatus.EXITING_RESCUE_MODE:
+ await asyncio.sleep(wait_interval)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.status == NodeStatus.FAILED_EXITING_RESCUE_MODE:
+ msg = "{hostname} failed to exit rescue mode.".format(
+ hostname=self.hostname
+ )
+ raise RescueModeFailure(msg, self)
+ return self
+
+ async def get_curtin_config(self):
+ """Get the curtin configuration.
+
+ :returns: Curtin configuration
+ :rtype: `str`
+ """
+ return self._handler.get_curtin_config(system_id=self.system_id)
+
+ async def get_details(self):
+ """Get machine details information.
+
+ :returns: Mapping of hardware details.
+ """
+ data = await self._handler.details(system_id=self.system_id)
+ return bson.decode_all(data)[0]
+
+ async def mark_broken(self, *, comment: str = None):
+ """Mark broken.
+
+ :param comment: Reason machine is broken.
+ :type comment: `str`
+ """
+ params = {"system_id": self.system_id}
+ if comment:
+ params["comment"] = comment
+ self._reset(await self._handler.mark_broken(**params))
+ return self
+
+ async def mark_fixed(self, *, comment: str = None):
+ """Mark fixes.
+
+ :param comment: Reason machine is fixed.
+ :type comment: `str`
+ """
+ params = {"system_id": self.system_id}
+ if comment:
+ params["comment"] = comment
+ self._reset(await self._handler.mark_fixed(**params))
+ return self
+
+ async def release(
+ self,
+ *,
+ comment: str = None,
+ erase: bool = None,
+ secure_erase: bool = None,
+ quick_erase: bool = None,
+ wait: bool = False,
+ wait_interval: int = 5
+ ):
+ """
+ Release the machine.
+
+ :param comment: Reason machine was released.
+ :type comment: `str`
+ :param erase: Erase the disk when release.
+ :type erase: `bool`
+ :param secure_erase: Use the drive's secure erase feature if available.
+ :type secure_erase: `bool`
+ :param quick_erase: Wipe the just the beginning and end of the disk.
+ This is not secure.
+ :param wait: If specified, wait until the deploy is complete.
+ :type wait: `bool`
+ :param wait_interval: How often to poll, defaults to 5 seconds.
+ :type wait_interval: `int`
+ """
+ params = remove_None(
+ {
+ "system_id": self.system_id,
+ "comment": comment,
+ "erase": erase,
+ "secure_erase": secure_erase,
+ "quick_erase": quick_erase,
+ }
+ )
+ self._reset(await self._handler.release(**params))
+ if not wait:
+ return self
+ else:
+ # Wait for machine to be released
+ while self.status in [NodeStatus.RELEASING, NodeStatus.DISK_ERASING]:
+ await asyncio.sleep(wait_interval)
+ try:
+ self._reset(await self._handler.read(system_id=self.system_id))
+ except CallError as error:
+ if error.status == HTTPStatus.NOT_FOUND:
+ # Release must have been on a machine in a pod. This
+ # machine no longer exists. Just return the machine
+ # as it has been released.
+ return self
+ else:
+ raise
+ if self.status == NodeStatus.FAILED_RELEASING:
+ msg = "{hostname} failed to be released.".format(hostname=self.hostname)
+ raise FailedReleasing(msg, self)
+ elif self.status == NodeStatus.FAILED_DISK_ERASING:
+ msg = "{hostname} failed to erase disk.".format(hostname=self.hostname)
+ raise FailedDiskErasing(msg, self)
+ return self
+
+ async def power_on(
+ self, comment: str = None, wait: bool = False, wait_interval: int = 5
+ ):
+ """
+ Power on.
- async def release(self, comment: str=None):
+ :param comment: Reason machine was powered on.
+ :type comment: `str`
+ :param wait: If specified, wait until the machine is powered on.
+ :type wait: `bool`
+ :param wait_interval: How often to poll, defaults to 5 seconds.
+ :type wait_interval: `int`
+ """
params = {"system_id": self.system_id}
if comment is not None:
params["comment"] = comment
- data = await self._handler.release(**params)
- return type(self)(data)
+ try:
+ self._reset(await self._handler.power_on(**params))
+ except CallError as error:
+ if error.status == HTTPStatus.FORBIDDEN:
+ message = "Not allowed to power on machine."
+ raise OperationNotAllowed(message) from error
+ else:
+ raise
+ if not wait or self.power_state == PowerState.UNKNOWN:
+ # Don't wait for a machine that always shows power state as
+ # unknown as the driver cannot query the power state.
+ return self
+ else:
+ # Wait for machine to be powered on.
+ while self.power_state == PowerState.OFF:
+ await asyncio.sleep(wait_interval)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.power_state == PowerState.ERROR:
+ msg = "{hostname} failed to power on.".format(hostname=self.hostname)
+ raise PowerError(msg, self)
+ return self
+
+ async def power_off(
+ self,
+ stop_mode: PowerStopMode = PowerStopMode.HARD,
+ comment: str = None,
+ wait: bool = False,
+ wait_interval: int = 5,
+ ):
+ """
+ Power off.
- def __repr__(self):
- return super(Machine, self).__repr__(
- fields={"system_id", "hostname"})
+ :param stop_mode: How to perform the power off.
+ :type stop_mode: `PowerStopMode`
+ :param comment: Reason machine was powered on.
+ :type comment: `str`
+ :param wait: If specified, wait until the machine is powered on.
+ :type wait: `bool`
+ :param wait_interval: How often to poll, defaults to 5 seconds.
+ :type wait_interval: `int`
+ """
+ params = {"system_id": self.system_id, "stop_mode": stop_mode.value}
+ if comment is not None:
+ params["comment"] = comment
+ try:
+ self._reset(await self._handler.power_off(**params))
+ except CallError as error:
+ if error.status == HTTPStatus.FORBIDDEN:
+ message = "Not allowed to power off machine."
+ raise OperationNotAllowed(message) from error
+ else:
+ raise
+ if not wait or self.power_state == PowerState.UNKNOWN:
+ # Don't wait for a machine that always shows power state as
+ # unknown as the driver cannot query the power state.
+ return self
+ else:
+ # Wait for machine to be powered off.
+ while self.power_state == PowerState.ON:
+ await asyncio.sleep(wait_interval)
+ self._reset(await self._handler.read(system_id=self.system_id))
+ if self.power_state == PowerState.ERROR:
+ msg = "{hostname} failed to power off.".format(hostname=self.hostname)
+ raise PowerError(msg, self)
+ return self
+
+ async def query_power_state(self):
+ """
+ Query the machine's BMC for the current power state.
+
+ :returns: Current power state.
+ :rtype: `PowerState`
+ """
+ power_data = await self._handler.query_power_state(system_id=self.system_id)
+ # Update the internal state of this object as well, since we have the
+ # updated power state from the BMC directly. MAAS server does this as
+ # well, just do it client side to make it nice for a developer.
+ self._data["power_state"] = power_data["state"]
+ return PowerState(power_data["state"])
+
+ async def restore_default_configuration(self):
+ """
+ Restore machine's configuration to its initial state.
+ """
+ self._reset(
+ await self._handler.restore_default_configuration(system_id=self.system_id)
+ )
+
+ async def restore_networking_configuration(self):
+ """
+ Restore machine's networking configuration to its initial state.
+ """
+ self._reset(
+ await self._handler.restore_networking_configuration(
+ system_id=self.system_id
+ )
+ )
+
+ async def restore_storage_configuration(self):
+ """
+ Restore machine's storage configuration to its initial state.
+ """
+ self._reset(
+ await self._handler.restore_storage_configuration(system_id=self.system_id)
+ )
+
+ async def lock(self, *, comment: str = None):
+ """Lock the machine to prevent changes.
+
+ :param comment: Reason machine was locked.
+ :type comment: `str`
+ """
+ params = {"system_id": self.system_id}
+ if comment:
+ params["comment"] = comment
+ self._reset(await self._handler.lock(**params))
+ return self
+
+ async def unlock(self, *, comment: str = None):
+ """Unlock the machine allowing changes.
+
+ :param comment: Reason machine was unlocked.
+ :type comment: `str`
+ """
+ params = {"system_id": self.system_id}
+ if comment:
+ params["comment"] = comment
+ self._reset(await self._handler.unlock(**params))
+ return self
diff --git a/maas/client/viscera/nodes.py b/maas/client/viscera/nodes.py
new file mode 100644
index 00000000..3b181471
--- /dev/null
+++ b/maas/client/viscera/nodes.py
@@ -0,0 +1,169 @@
+"""Objects for nodes."""
+
+__all__ = ["Node", "Nodes"]
+
+try:
+ # Python <= 3.9
+ from collections import Sequence
+except:
+ # Python > 3.9
+ from collections.abc import Sequence
+
+import typing
+
+from . import (
+ check,
+ Object,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ ObjectSet,
+ ObjectType,
+ to,
+)
+from ..enum import NodeType, PowerState
+
+
+def normalize_hostname(hostname):
+ """Strips the FQDN from the hostname, since hostname is unique in MAAS."""
+ if hostname:
+ return hostname.split(".", 1)[0]
+ return hostname
+
+
+def map_tag_name_to_dict(instance, value):
+ """Convert a tag name into a dictionary for Tag."""
+ return {"name": value, "__incomplete__": True}
+
+
+class NodesType(ObjectType):
+ """Metaclass for `Nodes`."""
+
+ async def read(cls, *, hostnames: typing.Sequence[str] = None):
+ """List nodes.
+
+ :param hostnames: Sequence of hostnames to only return.
+ :type hostnames: sequence of `str`
+ """
+ params = {}
+ if hostnames:
+ params["hostname"] = [
+ normalize_hostname(hostname) for hostname in hostnames
+ ]
+ data = await cls._handler.read(**params)
+ return cls(map(cls._object, data))
+
+
+class Nodes(ObjectSet, metaclass=NodesType):
+ """The set of nodes stored in MAAS."""
+
+
+class NodeTypeMeta(ObjectType):
+ """Metaclass for `Node`."""
+
+ async def read(cls, system_id):
+ data = await cls._handler.read(system_id=system_id)
+ return cls(data)
+
+
+class Node(Object, metaclass=NodeTypeMeta):
+ """A node stored in MAAS."""
+
+ domain = ObjectFieldRelated("domain", "Domain")
+ fqdn = ObjectField.Checked("fqdn", check(str), readonly=True)
+ hostname = ObjectField.Checked("hostname", check(str), check(str))
+ interfaces = ObjectFieldRelatedSet("interface_set", "Interfaces")
+ ip_addresses = ObjectField.Checked( # List[str]
+ "ip_addresses", check(Sequence), readonly=True
+ )
+ node_type = ObjectField.Checked("node_type", to(NodeType), readonly=True)
+ owner = ObjectFieldRelated("owner", "User")
+ power_state = ObjectField.Checked("power_state", to(PowerState), readonly=True)
+ power_type = ObjectField.Checked("power_type", check(str))
+ pool = ObjectFieldRelated("pool", "ResourcePool", use_data_setter=True)
+ system_id = ObjectField.Checked("system_id", check(str), readonly=True, pk=True)
+ tags = ObjectFieldRelatedSet(
+ "tag_names", "Tags", reverse=None, map_func=map_tag_name_to_dict
+ )
+ zone = ObjectFieldRelated("zone", "Zone")
+
+ def __repr__(self):
+ return super(Node, self).__repr__(fields={"system_id", "hostname"})
+
+ def as_machine(self):
+ """Convert to a `Machine` object.
+
+ `node_type` must be `NodeType.MACHINE`.
+ """
+ if self.node_type != NodeType.MACHINE:
+ raise ValueError("Cannot convert to `Machine`, node_type is not a machine.")
+ return self._origin.Machine(self._data)
+
+ def as_device(self):
+ """Convert to a `Device` object.
+
+ `node_type` must be `NodeType.DEVICE`.
+ """
+ if self.node_type != NodeType.DEVICE:
+ raise ValueError("Cannot convert to `Device`, node_type is not a device.")
+ return self._origin.Device(self._data)
+
+ def as_rack_controller(self):
+ """Convert to a `RackController` object.
+
+ `node_type` must be `NodeType.RACK_CONTROLLER` or
+ `NodeType.REGION_AND_RACK_CONTROLLER`.
+ """
+ if self.node_type not in [
+ NodeType.RACK_CONTROLLER,
+ NodeType.REGION_AND_RACK_CONTROLLER,
+ ]:
+ raise ValueError(
+ "Cannot convert to `RackController`, node_type is not a "
+ "rack controller."
+ )
+ return self._origin.RackController(self._data)
+
+ def as_region_controller(self):
+ """Convert to a `RegionController` object.
+
+ `node_type` must be `NodeType.REGION_CONTROLLER` or
+ `NodeType.REGION_AND_RACK_CONTROLLER`.
+ """
+ if self.node_type not in [
+ NodeType.REGION_CONTROLLER,
+ NodeType.REGION_AND_RACK_CONTROLLER,
+ ]:
+ raise ValueError(
+ "Cannot convert to `RegionController`, node_type is not a "
+ "region controller."
+ )
+ return self._origin.RegionController(self._data)
+
+ async def get_power_parameters(self):
+ """Get the power paramters for this node."""
+ data = await self._handler.power_parameters(system_id=self.system_id)
+ return data
+
+ async def set_power(
+ self, power_type: str, power_parameters: typing.Mapping[str, typing.Any] = {}
+ ):
+ """Set the power type and power parameters for this node."""
+ data = await self._handler.update(
+ system_id=self.system_id,
+ power_type=power_type,
+ power_parameters=power_parameters,
+ )
+ self.power_type = data["power_type"]
+
+ async def save(self):
+ # the resource pool uses the name in the API, not the id. The field is
+ # defined with use_data_setter=True, so the value in self._changed_data
+ # is the full data dict, not just the id.
+ if "pool" in self._changed_data:
+ self._changed_data["pool"] = self._changed_data["pool"]["name"]
+ return await super().save()
+
+ async def delete(self):
+ """Deletes the node from MAAS."""
+ await self._handler.delete(system_id=self.system_id)
diff --git a/maas/client/viscera/partitions.py b/maas/client/viscera/partitions.py
new file mode 100644
index 00000000..c29e250a
--- /dev/null
+++ b/maas/client/viscera/partitions.py
@@ -0,0 +1,161 @@
+"""Objects for partitions."""
+
+__all__ = ["Partition", "Partitions"]
+
+from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType
+from .nodes import Node
+from .block_devices import BlockDevice
+
+
+def map_device_id_to_dict(instance, value):
+ """Convert a device_id into a dictionary for BlockDevice."""
+ return {
+ "system_id": instance._data["system_id"],
+ "id": value,
+ "__incomplete__": True,
+ }
+
+
+class PartitionType(ObjectType):
+ """Metaclass for `Partition`."""
+
+ async def read(cls, node, block_device, id):
+ """Get `Partition` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ if isinstance(block_device, int):
+ block_device = block_device
+ elif isinstance(block_device, BlockDevice):
+ block_device = block_device.id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % type(block_device).__name__
+ )
+ return cls(
+ await cls._handler.read(system_id=system_id, device_id=block_device, id=id)
+ )
+
+
+class Partition(Object, metaclass=PartitionType):
+ """A partition on a block device."""
+
+ block_device = ObjectFieldRelated(
+ "device_id", "BlockDevice", readonly=True, pk=0, map_func=map_device_id_to_dict
+ )
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=1)
+ uuid = ObjectField.Checked("uuid", check(str), readonly=True)
+ path = ObjectField.Checked("path", check(str), readonly=True)
+ size = ObjectField.Checked("size", check(int), readonly=True)
+ used_for = ObjectField.Checked("used_for", check(str), readonly=True)
+
+ filesystem = ObjectFieldRelated("filesystem", "Filesystem", readonly=True)
+
+ def __repr__(self):
+ return super(Partition, self).__repr__(fields={"path", "size"})
+
+ async def delete(self):
+ """Delete this partition."""
+ await self._handler.delete(
+ system_id=self.block_device.node.system_id,
+ device_id=self.block_device.id,
+ id=self.id,
+ )
+
+ async def format(self, fstype, *, uuid=None):
+ """Format this partition."""
+ self._reset(
+ await self._handler.format(
+ system_id=self.block_device.node.system_id,
+ device_id=self.block_device.id,
+ id=self.id,
+ fstype=fstype,
+ uuid=uuid,
+ )
+ )
+
+ async def unformat(self):
+ """Unformat this partition."""
+ self._reset(
+ await self._handler.unformat(
+ system_id=self.block_device.node.system_id,
+ device_id=self.block_device.id,
+ id=self.id,
+ )
+ )
+
+ async def mount(self, mount_point, *, mount_options=None):
+ """Mount this partition."""
+ self._reset(
+ await self._handler.mount(
+ system_id=self.block_device.node.system_id,
+ device_id=self.block_device.id,
+ id=self.id,
+ mount_point=mount_point,
+ mount_options=mount_options,
+ )
+ )
+
+ async def umount(self):
+ """Unmount this partition."""
+ self._reset(
+ await self._handler.unmount(
+ system_id=self.block_device.node.system_id,
+ device_id=self.block_device.id,
+ id=self.id,
+ )
+ )
+
+
+class PartitionsType(ObjectType):
+ """Metaclass for `Partitions`."""
+
+ async def read(cls, node, block_device):
+ """Get list of `Partitions`'s for `node` and `block_device`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ if isinstance(block_device, int):
+ block_device = block_device
+ elif isinstance(block_device, BlockDevice):
+ block_device = block_device.id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % type(block_device).__name__
+ )
+ data = await cls._handler.read(system_id=system_id, device_id=block_device)
+ return cls(cls._object(item) for item in data)
+
+ async def create(cls, block_device: BlockDevice, size: int):
+ """
+ Create a partition on a block device.
+
+ :param block_device: BlockDevice to create the paritition on.
+ :type block_device: `BlockDevice`
+ :param size: The size of the partition in bytes.
+ :type size: `int`
+ """
+ params = {}
+ if isinstance(block_device, BlockDevice):
+ params["system_id"] = block_device.node.system_id
+ params["device_id"] = block_device.id
+ else:
+ raise TypeError(
+ "block_device must be a BlockDevice, not %s"
+ % (type(block_device).__name__)
+ )
+
+ if not size:
+ raise ValueError("size must be provided and greater than zero.")
+ params["size"] = size
+ return cls._object(await cls._handler.create(**params))
+
+
+class Partitions(ObjectSet, metaclass=PartitionsType):
+ """The set of partitions on a block device."""
diff --git a/maas/client/viscera/pods.py b/maas/client/viscera/pods.py
new file mode 100644
index 00000000..c2c0cdb9
--- /dev/null
+++ b/maas/client/viscera/pods.py
@@ -0,0 +1,235 @@
+"""Objects for pods."""
+
+__all__ = ["Pod", "Pods"]
+
+import typing
+from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType
+from .zones import Zone
+from ..utils import remove_None
+from ..errors import OperationNotAllowed
+
+
+class PodsType(ObjectType):
+ """Metaclass for `Pods`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(
+ cls,
+ *,
+ type: str,
+ power_address: str,
+ power_user: str = None,
+ power_pass: str = None,
+ name: str = None,
+ zone: typing.Union[str, Zone] = None,
+ tags: typing.Sequence[str] = None
+ ):
+ """Create a `Pod` in MAAS.
+
+ :param type: Type of pod to create (rsd, virsh) (required).
+ :type name: `str`
+ :param power_address: Address for power control of the pod (required).
+ :type power_address: `str`
+ :param power_user: User for power control of the pod
+ (required for rsd).
+ :type power_user: `str`
+ :param power_pass: Password for power control of the pod
+ (required for rsd).
+ :type power_pass: `str`
+ :param name: Name for the pod (optional).
+ :type name: `str`
+ :param zone: Name of the zone for the pod (optional).
+ :type zone: `str` or `Zone`
+ :param tags: A tag or tags (separated by comma) for the pod.
+ :type tags: `str`
+ :returns: The created Pod.
+ :rtype: `Pod`
+ """
+ params = remove_None(
+ {
+ "type": type,
+ "power_address": power_address,
+ "power_user": power_user,
+ "power_pass": power_pass,
+ "name": name,
+ "tags": tags,
+ }
+ )
+ if type == "rsd" and power_user is None:
+ message = "'power_user' is required for pod type `rsd`"
+ raise OperationNotAllowed(message)
+ if type == "rsd" and power_pass is None:
+ message = "'power_pass' is required for pod type `rsd`"
+ raise OperationNotAllowed(message)
+ if zone is not None:
+ if isinstance(zone, Zone):
+ params["zone"] = zone.name
+ elif isinstance(zone, str):
+ params["zone"] = zone
+ else:
+ raise TypeError(
+ "zone must be a str or Zone, not %s" % type(zone).__name__
+ )
+ return cls._object(await cls._handler.create(**params))
+
+
+class Pods(ObjectSet, metaclass=PodsType):
+ """The set of `Pods` stored in MAAS."""
+
+
+class PodType(ObjectType):
+ """Metaclass for a `Pod`."""
+
+ async def read(cls, id: int):
+ """Get `Pod` by `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class Pod(Object, metaclass=PodType):
+ """A `Pod` stored in MAAS."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ type = ObjectField.Checked("type", check(str), check(str))
+ name = ObjectField.Checked("name", check(str), check(str))
+ architectures = ObjectField.Checked("architectures", check(list), check(list))
+ capabilities = ObjectField.Checked("capabilities", check(list), check(list))
+ zone = ObjectFieldRelated("zone", "Zone", readonly=True)
+ tags = ObjectField.Checked("tags", check(list), check(list))
+ cpu_over_commit_ratio = ObjectField.Checked(
+ "cpu_over_commit_ratio", check(float), check(float)
+ )
+ memory_over_commit_ratio = ObjectField.Checked(
+ "memory_over_commit_ratio", check(float), check(float)
+ )
+ available = ObjectField.Checked("available", check(dict), check(dict))
+ used = ObjectField.Checked("used", check(dict), check(dict))
+ total = ObjectField.Checked("total", check(dict), check(dict))
+ default_macvlan_mode = ObjectField.Checked(
+ "default_macvlan_mode", check(str), check(str)
+ )
+ host = ObjectFieldRelated("host", "Node", readonly=True)
+
+ async def save(self):
+ """Save the `Pod`."""
+ old_tags = list(self._orig_data["tags"])
+ new_tags = list(self.tags)
+ self._changed_data.pop("tags", None)
+ await super(Pod, self).save()
+ for tag_name in new_tags:
+ if tag_name not in old_tags:
+ await self._handler.add_tag(id=self.id, tag=tag_name)
+ else:
+ old_tags.remove(tag_name)
+ for tag_name in old_tags:
+ await self._handler.remove_tag(id=self.id, tag=tag_name)
+ self._orig_data["tags"] = new_tags
+ self._data["tags"] = list(new_tags)
+
+ async def refresh(self):
+ """Refresh the `Pod`."""
+ return await self._handler.refresh(id=self.id)
+
+ async def parameters(self):
+ """Get the power parameters for the `Pod`."""
+ return await self._handler.parameters(id=self.id)
+
+ async def compose(
+ self,
+ *,
+ cores: int = None,
+ memory: int = None,
+ cpu_speed: int = None,
+ architecture: str = None,
+ storage: typing.Sequence[str] = None,
+ hostname: str = None,
+ domain: typing.Union[int, str] = None,
+ zone: typing.Union[int, str, Zone] = None,
+ interfaces: typing.Sequence[str] = None
+ ):
+ """Compose a machine from `Pod`.
+
+ All fields below are optional:
+
+ :param cores: Minimum number of CPU cores.
+ :type cores: `int`
+ :param memory: Minimum amount of memory (MiB).
+ :type memory: `int`
+ :param cpu_speed: Minimum amount of CPU speed (MHz).
+ :type cpu_speed: `int`
+ :param architecture: Architecture for the machine. Must be an
+ architecture that the pod supports.
+ :type architecture: `str`
+ :param storage: A list of storage constraint identifiers, in the form:
+ :([,[,...])][,:...]
+ :type storage: `str`
+ :param hostname: Hostname for the newly composed machine.
+ :type hostname: `str`
+ :param domain: The domain ID for the machine (optional).
+ :type domain: `int` or `str`
+ :param zone: The zone ID for the machine (optional).
+ :type zone: `int` or 'str' or `Zone`
+ :param interfaces: A labeled constraint map associating constraint
+ labels with interface properties that should be matched. Returned
+ nodes must have one or more interface matching the specified
+ constraints. The labeled constraint map must be in the format:
+ ``:=[,=[,...]]``
+
+ Each key can be one of the following:
+
+ - id: Matches an interface with the specific id
+ - fabric: Matches an interface attached to the specified fabric.
+ - fabric_class: Matches an interface attached to a fabric
+ with the specified class.
+ - ip: Matches an interface with the specified IP address
+ assigned to it.
+ - mode: Matches an interface with the specified mode. (Currently,
+ the only supported mode is "unconfigured".)
+ - name: Matches an interface with the specified name.
+ (For example, "eth0".)
+ - hostname: Matches an interface attached to the node with
+ the specified hostname.
+ - subnet: Matches an interface attached to the specified subnet.
+ - space: Matches an interface attached to the specified space.
+ - subnet_cidr: Matches an interface attached to the specified
+ subnet CIDR. (For example, "192.168.0.0/24".)
+ - type: Matches an interface of the specified type. (Valid
+ types: "physical", "vlan", "bond", "bridge", or "unknown".)
+ - vlan: Matches an interface on the specified VLAN.
+ - vid: Matches an interface on a VLAN with the specified VID.
+ - tag: Matches an interface tagged with the specified tag.
+ :type interfaces: `str`
+ :returns: The created Machine
+ :rtype: `Machine`
+ """
+ params = remove_None(
+ {
+ "cores": str(cores) if cores else None,
+ "memory": str(memory) if memory else None,
+ "cpu_speed": str(cpu_speed) if cpu_speed else None,
+ "architecture": architecture,
+ "storage": storage,
+ "hostname": hostname,
+ "domain": str(domain) if domain else None,
+ "interfaces": interfaces,
+ }
+ )
+ if zone is not None:
+ if isinstance(zone, Zone):
+ params["zone"] = str(zone.id)
+ elif isinstance(zone, int):
+ params["zone"] = str(zone)
+ elif isinstance(zone, str):
+ params["zone"] = zone
+ else:
+ raise TypeError(
+ "zone must be an int, str or Zone, not %s" % type(zone).__name__
+ )
+ return await self._handler.compose(**params, id=self.id)
+
+ async def delete(self):
+ """Delete the `Pod`."""
+ return await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/raids.py b/maas/client/viscera/raids.py
new file mode 100644
index 00000000..6d1e31dd
--- /dev/null
+++ b/maas/client/viscera/raids.py
@@ -0,0 +1,162 @@
+"""Objects for RAIDs."""
+
+__all__ = ["Raid", "Raids"]
+
+from typing import Iterable, Union
+
+from . import check, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType, to
+from .nodes import Node
+from .block_devices import BlockDevice
+from .partitions import Partition
+from .filesystem_groups import DevicesField, FilesystemGroup
+from ..enum import RaidLevel
+
+
+class RaidType(ObjectType):
+ """Metaclass for `Raid`."""
+
+ async def read(cls, node, id):
+ """Get `Raid` by `id`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ return cls(await cls._handler.read(system_id=system_id, id=id))
+
+
+class Raid(FilesystemGroup, metaclass=RaidType):
+ """A RAID on a machine."""
+
+ uuid = ObjectField.Checked("uuid", check(str), check(str))
+
+ level = ObjectField.Checked("level", to(RaidLevel), readonly=True)
+ size = ObjectField.Checked("size", check(int), check(int), readonly=True)
+
+ devices = DevicesField("devices")
+ spare_devices = DevicesField("spare_devices")
+ virtual_device = ObjectFieldRelated(
+ "virtual_device", "BlockDevice", reverse=None, readonly=True
+ )
+
+ def __repr__(self):
+ return super(Raid, self).__repr__(fields={"name", "level", "size"})
+
+ async def delete(self):
+ """Delete this RAID."""
+ await self._handler.delete(system_id=self.node.system_id, id=self.id)
+
+
+class RaidsType(ObjectType):
+ """Metaclass for `Raids`."""
+
+ async def read(cls, node):
+ """Get list of `Raid`'s for `node`."""
+ if isinstance(node, str):
+ system_id = node
+ elif isinstance(node, Node):
+ system_id = node.system_id
+ else:
+ raise TypeError("node must be a Node or str, not %s" % type(node).__name__)
+ data = await cls._handler.read(system_id=system_id)
+ return cls(
+ cls._object(item, local_data={"node_system_id": system_id}) for item in data
+ )
+
+ async def create(
+ cls,
+ node: Union[Node, str],
+ level: Union[RaidLevel, str],
+ devices: Iterable[Union[BlockDevice, Partition]],
+ *,
+ name: str = None,
+ uuid: str = None,
+ spare_devices: Iterable[Union[BlockDevice, Partition]]
+ ):
+ """
+ Create a RAID on a Node.
+
+ :param node: Node to create the interface on.
+ :type node: `Node` or `str`
+ :param level: RAID level.
+ :type level: `RaidLevel`
+ :param devices: Mixed list of block devices or partitions to create
+ the RAID from.
+ :type devices: iterable of mixed type of `BlockDevice` or `Partition`
+ :param name: Name of the RAID (optional).
+ :type name: `str`
+ :param uuid: The UUID for the RAID (optional).
+ :type uuid: `str`
+ :param spare_devices: Mixed list of block devices or partitions to add
+ as spare devices on the RAID.
+ :type spare_devices: iterable of mixed type of `BlockDevice` or
+ `Partition`
+ """
+ if isinstance(level, RaidLevel):
+ level = level.value
+ params = {"level": str(level)}
+ if isinstance(node, str):
+ params["system_id"] = node
+ elif isinstance(node, Node):
+ params["system_id"] = node.system_id
+ else:
+ raise TypeError(
+ "node must be a Node or str, not %s" % (type(node).__name__)
+ )
+
+ if len(devices) == 0:
+ raise ValueError("devices must contain at least one device.")
+
+ block_devices = []
+ partitions = []
+ for idx, device in enumerate(devices):
+ if isinstance(device, BlockDevice):
+ block_devices.append(device.id)
+ elif isinstance(device, Partition):
+ partitions.append(device.id)
+ else:
+ raise TypeError(
+ "devices[%d] must be a BlockDevice or "
+ "Partition, not %s" % type(device).__name__
+ )
+ if len(block_devices) > 0:
+ params["block_devices"] = block_devices
+ if len(partitions) > 0:
+ params["partitions"] = partitions
+
+ spare_block_devices = []
+ spare_partitions = []
+ for idx, device in enumerate(spare_devices):
+ if isinstance(device, BlockDevice):
+ spare_block_devices.append(device.id)
+ elif isinstance(device, Partition):
+ spare_partitions.append(device.id)
+ else:
+ raise TypeError(
+ "spare_devices[%d] must be a BlockDevice or "
+ "Partition, not %s" % type(device).__name__
+ )
+ if len(spare_block_devices) > 0:
+ params["spare_devices"] = spare_block_devices
+ if len(spare_partitions) > 0:
+ params["spare_partitions"] = spare_partitions
+
+ if name is not None:
+ params["name"] = name
+ if uuid is not None:
+ params["uuid"] = uuid
+ return cls._object(await cls._handler.create(**params))
+
+
+class Raids(ObjectSet, metaclass=RaidsType):
+ """The set of RAIDs on a machine."""
+
+ @property
+ def by_name(self):
+ """Return mapping of name to `Raid`."""
+ return {raid.name: raid for raid in self}
+
+ def get_by_name(self, name):
+ """Return a `Raid` by its name."""
+ return self.by_name[name]
diff --git a/maas/client/viscera/resource_pools.py b/maas/client/viscera/resource_pools.py
new file mode 100644
index 00000000..0a0e48d2
--- /dev/null
+++ b/maas/client/viscera/resource_pools.py
@@ -0,0 +1,57 @@
+"""Objects for resource pools."""
+
+__all__ = ["ResourcePool", "ResourcePools"]
+
+from . import check, Object, ObjectField, ObjectSet, ObjectType
+
+
+class ResourcePoolsType(ObjectType):
+ """Metaclass for `ResorucePools`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(cls, name: str, description: str = None):
+ """
+ Create a `ResourcePool` in MAAS.
+
+ :param name: The name of the `ResourcePool`.
+ :type name: `str`
+ :param description: A description of the `ResourcePool`.
+ :type description: `str`
+ :returns: The created `ResourcePool`
+ :rtype: `ResourcePool`
+ """
+ params = {"name": name}
+ if description is not None:
+ params["description"] = description
+ return cls._object(await cls._handler.create(**params))
+
+
+class ResourcePools(ObjectSet, metaclass=ResourcePoolsType):
+ """The set of resource pools stored in MAAS."""
+
+
+class ResourcePoolType(ObjectType):
+ async def read(cls, id):
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class ResourcePool(Object, metaclass=ResourcePoolType):
+ """A resource pool stored in MAAS."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+
+ name = ObjectField.Checked("name", check(str), check(str))
+ description = ObjectField.Checked("description", check(str), check(str))
+
+ def __repr__(self):
+ return super(ResourcePool, self).__repr__(fields={"name", "description"})
+
+ async def delete(self):
+ """
+ Deletes the `ResourcePool` from MAAS.
+ """
+ return await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/spaces.py b/maas/client/viscera/spaces.py
new file mode 100644
index 00000000..bfb27943
--- /dev/null
+++ b/maas/client/viscera/spaces.py
@@ -0,0 +1,73 @@
+"""Objects for spaces."""
+
+__all__ = ["Spaces", "Space"]
+
+from . import check, Object, ObjectField, ObjectFieldRelatedSet, ObjectSet, ObjectType
+
+
+class SpacesType(ObjectType):
+ """Metaclass for `Spaces`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(cls, *, name: str = None, description: str = None):
+ """
+ Create a `Space` in MAAS.
+
+ :param name: The name of the `Space` (optional, will be given a
+ default value if not specified).
+ :type name: `str`
+ :param description: A description of the `Space` (optional).
+ :type description: `str`
+ :returns: The created Space
+ :rtype: `Space`
+ """
+ params = {}
+ if name is not None:
+ params["name"] = name
+ if description is not None:
+ params["description"] = description
+ return cls._object(await cls._handler.create(**params))
+
+
+class Spaces(ObjectSet, metaclass=SpacesType):
+ """The set of Spaces stored in MAAS."""
+
+
+class SpaceType(ObjectType):
+
+ _default_space_id = 0
+
+ async def get_default(cls):
+ """Get the 'default' Space for the MAAS."""
+ data = await cls._handler.read(id=cls._default_space_id)
+ return cls(data)
+
+ async def read(cls, id: int):
+ """Get a `Space` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class Space(Object, metaclass=SpaceType):
+ """A Space."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ name = ObjectField.Checked("name", check(str), readonly=True, alt_pk=0)
+
+ # description is allowed in the create call and displayed in the UI
+ # but never returned by the API.
+
+ vlans = ObjectFieldRelatedSet("vlans", "Vlans", reverse=None)
+
+ async def delete(self):
+ """Delete this Space."""
+ if self.id == self._origin.Space._default_space_id:
+ raise DeleteDefaultSpace("Cannot delete default space.")
+ await self._handler.delete(id=self.id)
+
+
+class DeleteDefaultSpace(Exception):
+ """Default space cannot be deleted."""
diff --git a/maas/client/viscera/sshkeys.py b/maas/client/viscera/sshkeys.py
new file mode 100644
index 00000000..bcd74098
--- /dev/null
+++ b/maas/client/viscera/sshkeys.py
@@ -0,0 +1,49 @@
+"""Objects for SSH Keys."""
+
+__all__ = ["SSHKeys", "SSHKey"]
+
+from . import check, check_optional, Object, ObjectField, ObjectSet, ObjectType
+
+
+class SSHKeysType(ObjectType):
+ """Metaclass for `SSHKeys`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(cls, key: str):
+ """
+ Create an SSH key in MAAS with the content in `key`.
+
+ :param key: The content of the SSH key
+ :type key: `str`
+ :returns: The created SSH key
+ :rtype: `SSHKey`
+ """
+ return cls._object(await cls._handler.create(key=key))
+
+
+class SSHKeys(ObjectSet, metaclass=SSHKeysType):
+ """The set of SSH keys stored in MAAS."""
+
+
+class SSHKeyType(ObjectType):
+ async def read(cls, id: int):
+ """Get an `SSHKey` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class SSHKey(Object, metaclass=SSHKeyType):
+ """An SSH key."""
+
+ 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/static_routes.py b/maas/client/viscera/static_routes.py
new file mode 100644
index 00000000..f4809847
--- /dev/null
+++ b/maas/client/viscera/static_routes.py
@@ -0,0 +1,73 @@
+"""Objects for static_routes."""
+
+__all__ = ["StaticRoutes", "StaticRoute"]
+
+from . import check, Object, ObjectField, ObjectFieldRelated, ObjectSet, ObjectType
+from .subnets import Subnet
+from typing import Union
+
+
+class StaticRoutesType(ObjectType):
+ """Metaclass for `StaticRoutes`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(
+ cls,
+ destination: Union[int, Subnet],
+ source: Union[int, Subnet],
+ gateway_ip: str,
+ metric: int,
+ ):
+ """
+ Create a `StaticRoute` in MAAS.
+
+ :param name: The name of the `StaticRoute` (optional, will be given a
+ default value if not specified).
+ :type name: `str`
+ :param description: A description of the `StaticRoute` (optional).
+ :type description: `str`
+ :param class_type: The class type of the `StaticRoute` (optional).
+ :type class_type: `str`
+ :returns: The created StaticRoute
+ :rtype: `StaticRoute`
+ """
+ params = {"gateway_ip": gateway_ip, "metric": metric}
+ if isinstance(source, Subnet):
+ params["source"] = source.id
+ elif isinstance(source, int):
+ params["source"] = source
+ if isinstance(destination, Subnet):
+ params["destination"] = destination.id
+ elif isinstance(destination, int):
+ params["destination"] = destination
+ return cls._object(await cls._handler.create(**params))
+
+
+class StaticRoutes(ObjectSet, metaclass=StaticRoutesType):
+ """The set of StaticRoutes stored in MAAS."""
+
+
+class StaticRouteType(ObjectType):
+ """Metaclass for `StaticRoute`."""
+
+ async def read(cls, id: int):
+ """Get a `StaticRoute` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class StaticRoute(Object, metaclass=StaticRouteType):
+ """A StaticRoute."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ destination = ObjectFieldRelated("destination", "Subnet")
+ source = ObjectFieldRelated("source", "Subnet")
+ gateway_ip = ObjectField.Checked("gateway_ip", check(str))
+ metric = ObjectField.Checked("metric", check(int))
+
+ async def delete(self):
+ """Delete this StaticRoute."""
+ await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/subnets.py b/maas/client/viscera/subnets.py
new file mode 100644
index 00000000..f4da9fda
--- /dev/null
+++ b/maas/client/viscera/subnets.py
@@ -0,0 +1,142 @@
+"""Objects for subnets."""
+
+__all__ = ["Subnets", "Subnet"]
+
+from typing import Sequence, Union
+
+from . import (
+ check,
+ check_optional,
+ Object,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectSet,
+ ObjectType,
+ to,
+)
+from .vlans import Vlan
+from ..enum import RDNSMode
+
+
+class SubnetsType(ObjectType):
+ """Metaclass for `Subnets`."""
+
+ async def read(cls):
+ data = await cls._handler.read()
+ return cls(map(cls._object, data))
+
+ async def create(
+ cls,
+ cidr: str,
+ vlan: Union[Vlan, int] = None,
+ *,
+ name: str = None,
+ description: str = None,
+ gateway_ip: str = None,
+ rdns_mode: RDNSMode = None,
+ dns_servers: Union[Sequence[str], str] = None,
+ managed: bool = None
+ ):
+ """
+ Create a `Subnet` in MAAS.
+
+ :param cidr: The cidr of the `Subnet` (required).
+ :type cidr: `str`
+ :param vlan: The VLAN of the `Subnet` (required).
+ :type vlan: `Vlan`
+ :param name: The name of the `Subnet` (optional, will be given a
+ default value if not specified).
+ :type name: `str`
+ :param description: A description of the `Subnet` (optional).
+ :type description: `str`
+ :param gateway_ip: The gateway IP address for the `Subnet` (optional).
+ :type gateway_ip: `str`
+ :param rdns_mode: The reverse DNS mode for the `Subnet` (optional).
+ :type rdns_mode: `RDNSMode`
+ :param managed: Whether the `Subnet` is managed by MAAS (optional).
+ :type managed: `bool`
+ :returns: The created Subnet
+ :rtype: `Subnet`
+ """
+ params = {"cidr": cidr}
+
+ if isinstance(vlan, int):
+ params["vlan"] = vlan
+ elif isinstance(vlan, Vlan):
+ params["vlan"] = vlan.id
+ else:
+ raise TypeError("vlan must be Vlan or int, not %s" % (type(vlan).__class__))
+ if name is not None:
+ params["name"] = name
+ if description is not None:
+ params["description"] = description
+ if gateway_ip is not None:
+ params["gateway_ip"] = gateway_ip
+ if rdns_mode is not None:
+ params["rdns_mode"] = rdns_mode
+ if isinstance(dns_servers, Sequence):
+ if len(dns_servers) > 0:
+ params["dns_servers"] = ",".join(dns_servers)
+ elif dns_servers is not None:
+ params["dns_servers"] = dns_servers
+ if managed is not None:
+ params["managed"] = managed
+ return cls._object(await cls._handler.create(**params))
+
+
+class Subnets(ObjectSet, metaclass=SubnetsType):
+ """The set of Subnets stored in MAAS."""
+
+
+class SubnetType(ObjectType):
+ """Metaclass for `Subnet`"""
+
+ async def read(cls, id: int):
+ """Get a `Subnet` by its `id`."""
+ data = await cls._handler.read(id=id)
+ return cls(data)
+
+
+class Subnet(Object, metaclass=SubnetType):
+ """A Subnet."""
+
+ id = ObjectField.Checked("id", check(int), readonly=True, pk=True)
+ cidr = ObjectField.Checked("cidr", check(str), readonly=True)
+ name = ObjectField.Checked("name", check(str))
+
+ # description is allowed in the create call and displayed in the UI
+ # but never returned by the API
+
+ vlan = ObjectFieldRelated("vlan", "Vlan", use_data_setter=True)
+
+ # This should point to the space object and not just the string.
+ space = ObjectField.Checked("space", check(str))
+
+ active_discovery = ObjectField.Checked("active_discovery", check(bool))
+ allow_proxy = ObjectField.Checked("allow_proxy", check(bool))
+ managed = ObjectField.Checked("managed", check(bool))
+ gateway_ip = ObjectField.Checked("gateway_ip", check_optional(str))
+ rdns_mode = ObjectField.Checked("rdns_mode", to(RDNSMode))
+ dns_servers = ObjectField.Checked("dns_servers", check(list))
+
+ def __repr__(self):
+ return super(Subnet, self).__repr__(fields={"cidr", "name", "vlan"})
+
+ async def save(self):
+ """Save this subnet."""
+ if "vlan" in self._changed_data and self._changed_data["vlan"]:
+ # Update uses the ID of the VLAN, not the VLAN object.
+ self._changed_data["vlan"] = self._changed_data["vlan"]["id"]
+ if (
+ self._orig_data["vlan"]
+ and "id" in self._orig_data["vlan"]
+ and self._changed_data["vlan"] == (self._orig_data["vlan"]["id"])
+ ):
+ # VLAN didn't really change, the object was just set to the
+ # same VLAN.
+ del self._changed_data["vlan"]
+ await super(Subnet, self).save()
+
+ async def delete(self):
+ """Delete this Subnet."""
+ await self._handler.delete(id=self.id)
diff --git a/maas/client/viscera/tags.py b/maas/client/viscera/tags.py
index 3d13b973..f9a1ae51 100644
--- a/maas/client/viscera/tags.py
+++ b/maas/client/viscera/tags.py
@@ -1,27 +1,23 @@
"""Objects for tags."""
-__all__ = [
- "Tag",
- "Tags",
-]
+__all__ = ["Tag", "Tags"]
-from . import (
- check,
- check_optional,
- Object,
- ObjectField,
- ObjectSet,
- ObjectType,
-)
+from . import check, check_optional, Object, ObjectField, ObjectSet, ObjectType
+from .nodes import Node
class TagsType(ObjectType):
"""Metaclass for `Tags`."""
- async def create(cls, name, *, comment="", definition="", kernel_opts=""):
- data = await cls._handler.new(
- name=name, comment=comment, definition=definition,
- kernel_opts=kernel_opts)
+ async def create(cls, name, *, comment=None, definition=None, kernel_opts=None):
+ params = {"name": name}
+ if comment is not None:
+ params["comment"] = comment
+ if definition is not None:
+ params["definition"] = definition
+ if kernel_opts is not None:
+ params["kernel_opts"] = kernel_opts
+ data = await cls._handler.create(**params)
return cls._object(data)
async def read(cls):
@@ -32,16 +28,77 @@ async def read(cls):
class Tags(ObjectSet, metaclass=TagsType):
"""The set of tags."""
+ @classmethod
+ def Managed(cls, manager, field, items):
+ """Create a custom `Tags` that is managed by a related `Node.`
-class Tag(Object):
+ :param manager: The manager of the `ObjectSet`. This is the `Object`
+ that manages this set of objects.
+ :param field: The field on the `manager` that created this managed
+ `ObjectSet`.
+ :param items: The items in the `ObjectSet`.
+ """
+ if not isinstance(manager, Node):
+ raise TypeError(
+ "manager must be instance of Node, not %s", type(manager).__name__
+ )
+
+ async def add(self, tag: Tag):
+ """Add `tag` to node.
+
+ :param tag: Tag to add to the node.
+ :type tag: `Tag`.
+ """
+ if not isinstance(tag, Tag):
+ raise TypeError(
+ "tag must be instance of Tag, not %s", type(tag).__name__
+ )
+ await tag._handler.update_nodes(name=tag.name, add=manager.system_id)
+ if tag.name not in manager._data[field.name]:
+ manager._data[field.name] += [tag.name]
+
+ async def remove(self, tag: Tag):
+ """Remove `tag` from node.
+
+ :param tag: Tag to from the node.
+ :type tag: `Tag`.
+ """
+ if not isinstance(tag, Tag):
+ raise TypeError(
+ "tag must be instance of Tag, not %s", type(tag).__name__
+ )
+ await tag._handler.update_nodes(name=tag.name, remove=manager.system_id)
+ manager._data[field.name] = [
+ tag_name
+ for tag_name in manager._data[field.name]
+ if tag_name != tag.name
+ ]
+
+ attrs = {"add": add, "remove": remove}
+ cls = type(
+ "%s.Managed#%s" % (cls.__name__, manager.__class__.__name__), (cls,), attrs
+ )
+ return cls(items)
+
+
+class TagType(ObjectType):
+ async def read(cls, name):
+ data = await cls._handler.read(name=name)
+ return cls(data)
+
+
+class Tag(Object, metaclass=TagType):
"""A tag."""
- name = ObjectField.Checked(
- "name", check(str), readonly=True)
- comment = ObjectField.Checked(
- "comment", check(str), check(str), default="")
- definition = ObjectField.Checked(
- "definition", check(str), check(str), default="")
+ name = ObjectField.Checked("name", check(str), readonly=True, pk=True)
+ comment = ObjectField.Checked("comment", check(str), check(str), default="")
+ definition = ObjectField.Checked("definition", check(str), check(str), default="")
kernel_opts = ObjectField.Checked(
- "kernel_opts", check_optional(str), check_optional(str),
- default=None)
+ "kernel_opts", check_optional(str), check_optional(str), default=None
+ )
+
+ async def delete(self):
+ """
+ Deletes the `Tag` from MAAS.
+ """
+ return await self._handler.delete(name=self.name)
diff --git a/maas/client/viscera/testing.py b/maas/client/viscera/testing/__init__.py
similarity index 58%
rename from maas/client/viscera/testing.py
rename to maas/client/viscera/testing/__init__.py
index 216c6235..f11ed71e 100644
--- a/maas/client/viscera/testing.py
+++ b/maas/client/viscera/testing/__init__.py
@@ -1,17 +1,14 @@
""" Testing framework for maas.client.viscera """
-__all__ = [
- 'AsyncMock',
- 'Awaitable',
- 'bind',
-]
+__all__ = ["bind"]
-from functools import partial
+
+from collections.abc import Mapping
from itertools import chain
-from typing import Mapping
from unittest.mock import Mock
-from . import OriginBase
+from .. import OriginBase
+from ...testing import AsyncCallableMock
def bind(*objects, session=None):
@@ -31,6 +28,7 @@ def bind(*objects, session=None):
:param session: A `bones.SessionAPI` instance.
:return: An `OriginBase` instance.
"""
+
def _flatten_to_items(thing):
if isinstance(thing, Mapping):
yield from thing.items()
@@ -44,33 +42,7 @@ def _flatten_to_items(thing):
if session is None:
session = Mock(name="session")
session.handlers = {
- name: AsyncMock(name="handler(%s)" % name)
- for name in objects
+ name: AsyncCallableMock(name="handler(%s)" % name) for name in objects
}
return OriginBase(session, objects=objects)
-
-
-class AsyncMock(Mock):
- """Mock that is "future-like"; see PEP-492 for the details.
-
- The new `await` syntax chokes on arguments that are not future-like, i.e.
- have an `__await__` call, so we have to fool it.
- """
-
- def __call__(_mock_self, *args, **kwargs):
- callup = super(AsyncMock, _mock_self).__call__
- call = partial(callup, *args, **kwargs)
- return Awaitable(call)
-
-
-class Awaitable:
- """Wrap a "normal" call in a future-like object."""
-
- def __init__(self, call):
- super(Awaitable, self).__init__()
- self._call = call
-
- def __await__(self):
- yield # This serves only to make this a generator.
- return self._call()
diff --git a/maas/client/viscera/tests/test.py b/maas/client/viscera/tests/test.py
new file mode 100644
index 00000000..62611720
--- /dev/null
+++ b/maas/client/viscera/tests/test.py
@@ -0,0 +1,1319 @@
+"""Tests for `maas.client.viscera`."""
+
+from random import randrange, randint
+from unittest.mock import call, Mock, sentinel
+
+from testtools.matchers import (
+ Contains,
+ ContainsAll,
+ Equals,
+ HasLength,
+ Is,
+ IsInstance,
+ MatchesStructure,
+ Not,
+)
+
+from .. import (
+ check,
+ dir_class,
+ dir_instance,
+ Disabled,
+ Object,
+ ObjectBasics,
+ ObjectField,
+ ObjectFieldRelated,
+ ObjectFieldRelatedSet,
+ ObjectMethod,
+ ObjectSet,
+ ObjectType,
+ OriginBase,
+)
+from ... import bones, viscera
+from ...errors import ObjectNotLoaded
+from ...testing import AsyncCallableMock, make_name, make_name_without_spaces, TestCase
+from ...utils.tests.test_profiles import make_profile
+
+
+class TestDirClass(TestCase):
+ """Tests for `dir_class`."""
+
+ def test__includes_ObjectMethod_descriptors_with_class_methods(self):
+ class Example:
+ attribute = ObjectMethod(sentinel.function)
+
+ self.assertThat(list(dir_class(Example)), Contains("attribute"))
+
+ def test__excludes_ObjectMethod_descriptors_without_class_methods(self):
+ class Example:
+ attribute = ObjectMethod()
+
+ self.assertThat(list(dir_class(Example)), Not(Contains("attribute")))
+
+ def test__excludes_Disabled_class_descriptors(self):
+ class Example:
+ attribute = Disabled("foobar")
+
+ self.assertThat(list(dir_class(Example)), Not(Contains("attribute")))
+
+ def test__includes_other_class_attributes(self):
+ class Example:
+ alice = "is the first"
+ bob = lambda self: "just bob"
+ carol = classmethod(lambda cls: "carol")
+ dave = property(lambda self: "dave or david")
+ erin = staticmethod(lambda: "or eve?")
+
+ self.assertThat(
+ list(dir_class(Example)),
+ ContainsAll(["alice", "bob", "carol", "dave", "erin"]),
+ )
+
+ def test__excludes_mro_metaclass_method(self):
+ class Example:
+ """Example class."""
+
+ self.assertThat(list(dir_class(Example)), Not(Contains("mro")))
+
+ def test__excludes_Disabled_metaclass_descriptors(self):
+ class ExampleType(type):
+ attribute = Disabled("foobar")
+
+ class Example(metaclass=ExampleType):
+ """Example class with metaclass."""
+
+ self.assertThat(list(dir_class(Example)), Not(Contains("attribute")))
+
+ def test__includes_superclass_attributes(self):
+ class ExampleBase:
+ alice = "is the first"
+ bob = lambda self: "just bob"
+
+ class Example(ExampleBase):
+ carol = classmethod(lambda cls: "carol")
+ dave = property(lambda self: "dave or david")
+ erin = staticmethod(lambda: "or eve?")
+
+ self.assertThat(
+ list(dir_class(Example)),
+ ContainsAll(["alice", "bob", "carol", "dave", "erin"]),
+ )
+
+
+class TestDirInstance(TestCase):
+ """Tests for `dir_instance`."""
+
+ def test__includes_ObjectMethod_descriptors_with_instance_methods(self):
+ class Example:
+ attribute = ObjectMethod()
+ attribute.instancemethod(sentinel.function)
+
+ example = Example()
+
+ self.assertThat(list(dir_instance(example)), Contains("attribute"))
+
+ def test__excludes_ObjectMethod_descriptors_without_instance_methods(self):
+ class Example:
+ attribute = ObjectMethod()
+ attribute.classmethod(sentinel.function)
+
+ example = Example()
+
+ self.assertThat(list(dir_instance(example)), Not(Contains("attribute")))
+
+ def test__excludes_Disabled_class_descriptors(self):
+ class Example:
+ attribute = Disabled("foobar")
+
+ example = Example()
+
+ self.assertThat(list(dir_instance(example)), Not(Contains("attribute")))
+
+ def test__excludes_class_methods(self):
+ class Example:
+ carol = classmethod(lambda cls: "carol")
+
+ example = Example()
+
+ self.assertThat(list(dir_instance(example)), Not(Contains("carol")))
+
+ def test__excludes_static_methods(self):
+ class Example:
+ steve = staticmethod(lambda: "or eve?")
+
+ example = Example()
+
+ self.assertThat(list(dir_instance(example)), Not(Contains("steve")))
+
+ def test__includes_other_class_attributes(self):
+ class Example:
+ alice = "is the first"
+ bob = lambda self: "just bob"
+ dave = property(lambda self: "or david")
+
+ example = Example()
+
+ self.assertThat(
+ list(dir_instance(example)), ContainsAll(["alice", "bob", "dave"])
+ )
+
+ def test__excludes_instance_attributes(self):
+ # In a bit of a departure, dir_instance(foo) will NOT return instance
+ # attributes of foo. This is because object attributes in viscera
+ # should be defined using descriptors (which are class attributes).
+
+ class Example:
+ """Example class."""
+
+ example = Example()
+ example.alice = 123
+
+ self.assertThat(list(dir_instance(example)), Not(Contains(["alice"])))
+
+
+class TestObjectType(TestCase):
+ """Tests for `ObjectType`."""
+
+ def test__classes_always_have_slots_defined(self):
+ class WithoutSlots(metaclass=ObjectType):
+ """A class WITHOUT __slots__ defined explicitly."""
+
+ self.assertThat(WithoutSlots.__slots__, Equals(()))
+ self.assertRaises(AttributeError, getattr, WithoutSlots(), "__dict__")
+
+ class WithSlots(metaclass=ObjectType):
+ """A class WITH __slots__ defined explicitly."""
+
+ __slots__ = "a", "b"
+
+ self.assertThat(WithSlots.__slots__, Equals(("a", "b")))
+ self.assertRaises(AttributeError, getattr, WithSlots(), "__dict__")
+
+ def test__uses_dir_class(self):
+ class Dummy(metaclass=ObjectType):
+ """Does nothing; just a stand-in."""
+
+ dir_class = self.patch(viscera, "dir_class")
+ dir_class.return_value = iter([sentinel.name])
+
+ self.assertThat(dir(Dummy), Equals([sentinel.name]))
+
+
+class TestObjectBasics(TestCase):
+ """Tests for `ObjectBasics`."""
+
+ def test__defines_slots(self):
+ self.assertThat(ObjectBasics.__slots__, Equals(()))
+
+ def test__uses_dir_instance(self):
+ dir_instance = self.patch(viscera, "dir_instance")
+ dir_instance.return_value = iter([sentinel.name])
+
+ self.assertThat(dir(ObjectBasics()), Equals([sentinel.name]))
+
+ def test__stringification_returns_qualified_class_name(self):
+ self.assertThat(str(ObjectBasics()), Equals(ObjectBasics.__qualname__))
+
+
+class TestObject(TestCase):
+ """Tests for `Object`."""
+
+ def test__defines_slots(self):
+ self.assertThat(
+ Object.__slots__,
+ Equals(("__weakref__", "_data", "_orig_data", "_changed_data", "_loaded")),
+ )
+
+ def test__inherits_ObjectBasics(self):
+ self.assertThat(Object.__mro__, Contains(ObjectBasics))
+
+ def test__init_sets__data_and_loaded(self):
+ data = {"alice": make_name_without_spaces("alice")}
+ self.assertThat(Object(data)._data, Equals(data))
+ self.assertTrue(Object(data)._loaded)
+
+ def test__init_insists_on_mapping_when_no_pk(self):
+ error = self.assertRaises(TypeError, Object, ["some", "items"])
+ self.assertThat(str(error), Equals("data must be a mapping, not list"))
+
+ def test__init_insists_on_complete_data(self):
+ data = {"alice": make_name_without_spaces("alice"), "__incomplete__": True}
+ error = self.assertRaises(ValueError, Object, data)
+ self.assertThat(
+ str(error),
+ Equals("data cannot be incomplete without any primary keys defined"),
+ )
+
+ def test__init_takes_pk_when_defined(self):
+ object_type = type("PKObject", (Object,), {"pk": ObjectField("pk_d", pk=True)})
+ object_pk = randint(0, 20)
+ object_a = object_type(object_pk)
+ self.assertThat(object_a._data, Equals({"pk_d": object_pk}))
+ self.assertThat(object_a.pk, Equals(object_pk))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_takes_pk_in_mapping_when_defined(self):
+ object_type = type("PKObject", (Object,), {"pk": ObjectField("pk_d", pk=True)})
+ object_pk = randint(0, 20)
+ object_a = object_type({"pk_d": object_pk, "__incomplete__": True})
+ self.assertThat(object_a._data, Equals({"pk_d": object_pk}))
+ self.assertThat(object_a.pk, Equals(object_pk))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_takes_alt_pk_in_mapping_when_defined(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk": ObjectField("pk_d", pk=True),
+ "alt_pk": ObjectField("alt_pk_d", alt_pk=True),
+ },
+ )
+ object_alt_pk = randint(0, 20)
+ object_a = object_type({"alt_pk_d": object_alt_pk, "__incomplete__": True})
+ self.assertThat(object_a._data, Equals({"alt_pk_d": object_alt_pk}))
+ self.assertThat(object_a.alt_pk, Equals(object_alt_pk))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_validates_pk_when_defined(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk": ObjectField.Checked("pk_d", check(int), pk=True)},
+ )
+ error = self.assertRaises(TypeError, object_type, "not int")
+ self.assertThat(str(error), Equals("'not int' is not of type %r" % int))
+
+ def test__init_validates_pk_in_mapping_when_defined(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk": ObjectField.Checked("pk_d", check(int), pk=True)},
+ )
+ error = self.assertRaises(
+ TypeError, object_type, {"pk_d": "not int", "__incomplete__": True}
+ )
+ self.assertThat(str(error), Equals("'not int' is not of type %r" % int))
+
+ def test__init_validates_alt_pk_in_mapping_when_defined(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk": ObjectField.Checked("pk_d", check(int), pk=True),
+ "alt_pk": ObjectField.Checked("alt_pk_d", check(int), alt_pk=True),
+ },
+ )
+ error = self.assertRaises(
+ TypeError, object_type, {"alt_pk_d": "not int", "__incomplete__": True}
+ )
+ self.assertThat(str(error), Equals("'not int' is not of type %r" % int))
+
+ def test__init_doesnt_allow_multiple_pk_True(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=True),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=True),
+ },
+ )
+ error = self.assertRaises(AttributeError, object_type, [0, 1])
+ self.assertThat(
+ str(error),
+ Equals("more than one field is marked as unique " "primary key: pk1, pk2"),
+ )
+
+ def test__init_allows_mapping_when_multiple_pks(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ },
+ )
+ object_pk_1 = randint(0, 20)
+ object_pk_2 = randint(0, 20)
+ object_a = object_type(
+ {"pk_1": object_pk_1, "pk_2": object_pk_2, "__incomplete__": True}
+ )
+ self.assertThat(
+ object_a._data, Equals({"pk_1": object_pk_1, "pk_2": object_pk_2})
+ )
+ self.assertThat(object_a.pk1, Equals(object_pk_1))
+ self.assertThat(object_a.pk2, Equals(object_pk_2))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_allows_mapping_when_multiple_alt_pks(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ "alt_pk1": ObjectField.Checked("alt_pk_1", check(int), alt_pk=0),
+ "alt_pk2": ObjectField.Checked("alt_pk_2", check(int), alt_pk=1),
+ },
+ )
+ object_alt_pk_1 = randint(0, 20)
+ object_alt_pk_2 = randint(0, 20)
+ object_a = object_type(
+ {
+ "alt_pk_1": object_alt_pk_1,
+ "alt_pk_2": object_alt_pk_2,
+ "__incomplete__": True,
+ }
+ )
+ self.assertThat(
+ object_a._data,
+ Equals({"alt_pk_1": object_alt_pk_1, "alt_pk_2": object_alt_pk_2}),
+ )
+ self.assertThat(object_a.alt_pk1, Equals(object_alt_pk_1))
+ self.assertThat(object_a.alt_pk2, Equals(object_alt_pk_2))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_allows_sequence_when_multiple_pks(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ },
+ )
+ object_pk_1 = randint(0, 20)
+ object_pk_2 = randint(0, 20)
+ object_a = object_type([object_pk_1, object_pk_2])
+ self.assertThat(
+ object_a._data, Equals({"pk_1": object_pk_1, "pk_2": object_pk_2})
+ )
+ self.assertThat(object_a.pk1, Equals(object_pk_1))
+ self.assertThat(object_a.pk2, Equals(object_pk_2))
+ self.assertFalse(object_a._loaded)
+
+ def test__init_requires_mapping_or_sequence_when_multiple_pks(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ },
+ )
+ error = self.assertRaises(TypeError, object_type, 0)
+ self.assertThat(
+ str(error), Equals("data must be a mapping or a sequence, not int")
+ )
+
+ def test__init_validates_property_when_multiple_pks_mapping(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ },
+ )
+ error = self.assertRaises(
+ TypeError, object_type, {"pk_1": 0, "pk_2": "bad", "__incomplete__": True}
+ )
+ self.assertThat(str(error), Equals("'bad' is not of type %r" % int))
+
+ def test__init_validates_property_when_multiple_alt_pks_mapping(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ "alt_pk1": ObjectField.Checked("alt_pk_1", check(int), alt_pk=0),
+ "alt_pk2": ObjectField.Checked("alt_pk_2", check(int), alt_pk=1),
+ },
+ )
+ error = self.assertRaises(
+ TypeError,
+ object_type,
+ {"alt_pk_1": 0, "alt_pk_2": "bad", "__incomplete__": True},
+ )
+ self.assertThat(str(error), Equals("'bad' is not of type %r" % int))
+
+ def test__init_validates_property_when_multiple_pks_sequence(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField.Checked("pk_1", check(int), pk=0),
+ "pk2": ObjectField.Checked("pk_2", check(int), pk=1),
+ },
+ )
+ error = self.assertRaises(TypeError, object_type, [0, "bad"])
+ self.assertThat(str(error), Equals("'bad' is not of type %r" % int))
+
+ def test__loaded(self):
+ object_a = Object({})
+ self.assertTrue(object_a.loaded)
+ object_a._loaded = False
+ self.assertFalse(object_a.loaded)
+
+ def test__cannot_access_attributes_when_unloaded(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk": ObjectField("pk_d", pk=True), "name": ObjectField("name")},
+ )
+ object_pk = randint(0, 20)
+ object_a = object_type(object_pk)
+ object_a._data["name"] = make_name("name")
+ self.assertFalse(object_a.loaded)
+ error = self.assertRaises(ObjectNotLoaded, getattr, object_a, "name")
+ self.assertThat(
+ str(error), Equals("cannot access attribute 'name' of object 'PKObject'")
+ )
+
+ def test__can_access_pk_attributes_when_unloaded(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk": ObjectField("pk_d", pk=True), "name": ObjectField("name")},
+ )
+ object_pk = randint(0, 20)
+ object_a = object_type(object_pk)
+ self.assertFalse(object_a.loaded)
+ self.assertEquals(object_pk, object_a.pk)
+
+ def test__can_access_alt_pk_attributes_when_unloaded(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk": ObjectField("pk_d", pk=True),
+ "alt_pk": ObjectField("alt_pk_d", alt_pk=True),
+ "name": ObjectField("name"),
+ },
+ )
+ object_alt_pk = randint(0, 20)
+ object_a = object_type({"alt_pk_d": object_alt_pk, "__incomplete__": True})
+ self.assertFalse(object_a.loaded)
+ self.assertEquals(object_alt_pk, object_a.alt_pk)
+
+ def test__can_access_attributes_when_loaded(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk": ObjectField("pk_d", pk=True), "name": ObjectField("name")},
+ )
+ object_pk = randint(0, 20)
+ object_name = make_name("name")
+ object_a = object_type({"pk_d": object_pk, "name": object_name})
+ self.assertTrue(object_a.loaded)
+ self.assertEquals(object_name, object_a.name)
+
+ def test__equal_when_data_matches(self):
+ data = {"key": make_name("value")}
+ object_a = Object(data)
+ object_b = Object(data)
+ self.assertThat(object_a, Equals(object_b))
+ self.assertThat(object_b, Equals(object_a))
+
+ def test__not_equal_when_types_different(self):
+ # Even if one is a subclass of the other.
+ data = {"key": make_name("value")}
+ object_a = Object(data)
+ object_b = type("Object", (Object,), {})(data)
+ self.assertThat(object_a, Not(Equals(object_b)))
+ self.assertThat(object_b, Not(Equals(object_a)))
+
+ def test__string_representation_includes_field_values(self):
+ class Example(Object):
+ alice = ObjectField("alice")
+ bob = ObjectField("bob")
+
+ example = Example(
+ {
+ "alice": make_name_without_spaces("alice"),
+ "bob": make_name_without_spaces("bob"),
+ }
+ )
+
+ self.assertThat(
+ repr(example),
+ Equals("" % example._data),
+ )
+
+ def test__string_representation_can_be_limited_to_selected_fields(self):
+ class Example(Object):
+ alice = ObjectField("alice")
+ bob = ObjectField("bob")
+
+ example = Example(
+ {
+ "alice": make_name_without_spaces("alice"),
+ "bob": make_name_without_spaces("bob"),
+ }
+ )
+
+ # A string repr can be prepared using only the "alice" field.
+ self.assertThat(
+ example.__repr__(fields={"alice"}),
+ Equals("" % example._data),
+ )
+
+ # Fields are always displayed in a stable order though.
+ self.assertThat(
+ example.__repr__(fields=["bob", "alice"]),
+ Equals("" % example._data),
+ )
+
+ def test_refresh_raises_AttributeError_when_no_read_defined(self):
+ object_a = Object({})
+ error = self.assertRaises(AttributeError, object_a.refresh)
+ self.assertThat(str(error), Equals("'Object' object doesn't support refresh."))
+
+ def test_refresh_with_one_pk(self):
+ object_type = type("PKObject", (Object,), {"pk": ObjectField("pk_d", pk=True)})
+ object_pk = randint(0, 20)
+ new_data = {"pk_d": object_pk, "other": randint(0, 20)}
+ mock_read = AsyncCallableMock(return_value=object_type(new_data))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type(object_pk)
+ self.assertFalse(object_a.loaded)
+ object_a.refresh()
+ self.assertTrue(object_a.loaded)
+ self.assertThat(object_a._data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Not(Is(object_a._data)))
+ self.assertThat(object_a._changed_data, Equals({}))
+ self.assertThat(mock_read.call_args_list, Equals([call(object_pk)]))
+
+ def test_refresh_with_one_alt_pk(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk": ObjectField("pk_d", pk=True),
+ "alt_pk": ObjectField("alt_pk_d", alt_pk=True),
+ },
+ )
+ object_pk = randint(0, 20)
+ object_alt_pk = randint(0, 20)
+ new_data = {
+ "pk_d": object_pk,
+ "alt_pk_d": object_alt_pk,
+ "other": randint(0, 20),
+ }
+ mock_read = AsyncCallableMock(return_value=object_type(new_data))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type({"alt_pk_d": object_alt_pk, "__incomplete__": True})
+ self.assertFalse(object_a.loaded)
+ object_a.refresh()
+ self.assertTrue(object_a.loaded)
+ self.assertThat(object_a._data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Not(Is(object_a._data)))
+ self.assertThat(object_a._changed_data, Equals({}))
+ self.assertThat(mock_read.call_args_list, Equals([call(object_alt_pk)]))
+
+ def test_refresh_with_multiple_pk(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {"pk1": ObjectField("pk_1", pk=0), "pk2": ObjectField("pk_2", pk=1)},
+ )
+ object_pk_1 = randint(0, 20)
+ object_pk_2 = randint(0, 20)
+ new_data = {"pk_1": object_pk_1, "pk_2": object_pk_2, "other": randint(0, 20)}
+ mock_read = AsyncCallableMock(return_value=object_type(new_data))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type([object_pk_1, object_pk_2])
+ self.assertFalse(object_a.loaded)
+ object_a.refresh()
+ self.assertTrue(object_a.loaded)
+ self.assertThat(object_a._data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Not(Is(new_data)))
+ self.assertThat(object_a._changed_data, Equals({}))
+ self.assertThat(
+ mock_read.call_args_list, Equals([call(object_pk_1, object_pk_2)])
+ )
+
+ def test_refresh_with_multiple_alt_pk(self):
+ object_type = type(
+ "PKObject",
+ (Object,),
+ {
+ "pk1": ObjectField("pk_1", pk=0),
+ "pk2": ObjectField("pk_2", pk=1),
+ "alt_pk1": ObjectField("alt_pk_1", alt_pk=0),
+ "alt_pk2": ObjectField("alt_pk_2", alt_pk=1),
+ },
+ )
+ object_pk_1 = randint(0, 20)
+ object_pk_2 = randint(0, 20)
+ object_alt_pk_1 = randint(0, 20)
+ object_alt_pk_2 = randint(0, 20)
+ new_data = {
+ "pk_1": object_pk_1,
+ "pk_2": object_pk_2,
+ "alt_pk_1": object_alt_pk_1,
+ "alt_pk_2": object_alt_pk_2,
+ "other": randint(0, 20),
+ }
+ mock_read = AsyncCallableMock(return_value=object_type(new_data))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type(
+ {
+ "alt_pk_1": object_alt_pk_1,
+ "alt_pk_2": object_alt_pk_2,
+ "__incomplete__": True,
+ }
+ )
+ self.assertFalse(object_a.loaded)
+ object_a.refresh()
+ self.assertTrue(object_a.loaded)
+ self.assertThat(object_a._data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Equals(new_data))
+ self.assertThat(object_a._orig_data, Not(Is(new_data)))
+ self.assertThat(object_a._changed_data, Equals({}))
+ self.assertThat(
+ mock_read.call_args_list, Equals([call(object_alt_pk_1, object_alt_pk_2)])
+ )
+
+ def test_refresh_raises_AttributeError_when_no_pk_fields(self):
+ object_type = type("PKObject", (Object,), {})
+ mock_read = AsyncCallableMock(return_value=object_type({}))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type({})
+ error = self.assertRaises(AttributeError, object_a.refresh)
+ self.assertThat(
+ str(error),
+ Equals("unable to perform 'refresh' no primary key fields defined."),
+ )
+
+ def test_refresh_raises_TypeError_on_mismatch(self):
+ object_type = type("PKObject", (Object,), {"pk": ObjectField("pk_d", pk=True)})
+ mock_read = AsyncCallableMock(return_value=Object({}))
+ self.patch(object_type, "read", mock_read)
+ object_a = object_type({"pk_d": 0})
+ error = self.assertRaises(TypeError, object_a.refresh)
+ self.assertThat(
+ str(error),
+ Equals("result of 'PKObject.read' must be 'PKObject', not 'Object'"),
+ )
+
+ def test_save_raises_AttributeError_when_handler_has_no_update(self):
+ object_type = type("NotSaveableObject", (Object,), {"_handler": object()})
+ error = self.assertRaises(AttributeError, object_type({}).save)
+ self.assertThat(
+ str(error), Equals("'NotSaveableObject' object doesn't support save.")
+ )
+
+ def test_save_does_nothing_when_nothing_changed(self):
+ handler = Mock()
+ handler.update = AsyncCallableMock(return_value={})
+ object_type = type(
+ "SaveableObject",
+ (Object,),
+ {"_handler": handler, "name": ObjectField("name")},
+ )
+ object_a = object_type({})
+ object_a.save()
+ self.assertThat(handler.update.call_count, Equals(0))
+
+ def test_save_calls_update_on_handler_with_params(self):
+ object_id = randint(0, 10)
+ saved_name = make_name("name")
+ updated_data = {"id": object_id, "name": saved_name}
+ handler = Mock()
+ handler.params = ["id"]
+ handler.update = AsyncCallableMock(return_value=updated_data)
+ object_type = type(
+ "SaveableObject",
+ (Object,),
+ {"_handler": handler, "name": ObjectField("name")},
+ )
+ object_a = object_type({"id": object_id})
+ new_name = make_name("new")
+ object_a.name = new_name
+ object_a.save()
+ self.assertThat(
+ handler.update.call_args_list, Equals([call(id=object_id, name=new_name)])
+ )
+ self.assertThat(object_a._data, Equals(updated_data))
+ self.assertThat(object_a._orig_data, Equals(updated_data))
+ self.assertThat(object_a._orig_data, Not(Is(object_a._data)))
+ self.assertThat(object_a._changed_data, Equals({}))
+
+
+class TestObjectSet(TestCase):
+ """Tests for `ObjectSet`."""
+
+ def test__defines_slots(self):
+ self.assertThat(ObjectSet.__slots__, Equals(("__weakref__", "_items")))
+
+ def test__inherits_ObjectBasics(self):
+ self.assertThat(ObjectSet.__mro__, Contains(ObjectBasics))
+
+ def test__init_sets__items_from_sequence(self):
+ items = [{"alice": make_name_without_spaces("alice")}]
+ self.assertThat(ObjectSet(items)._items, Equals(items))
+
+ def test__init_sets__items_from_iterable(self):
+ items = [{"alice": make_name_without_spaces("alice")}]
+ self.assertThat(ObjectSet(iter(items))._items, Equals(items))
+
+ def test__init_rejects_mapping(self):
+ error = self.assertRaises(TypeError, ObjectSet, {})
+ self.assertThat(str(error), Equals("data must be sequence-like, not dict"))
+
+ def test__init_rejects_str(self):
+ error = self.assertRaises(TypeError, ObjectSet, "")
+ self.assertThat(str(error), Equals("data must be sequence-like, not str"))
+
+ def test__init_rejects_bytes(self):
+ error = self.assertRaises(TypeError, ObjectSet, b"")
+ self.assertThat(str(error), Equals("data must be sequence-like, not bytes"))
+
+ def test__init_rejects_non_iterable(self):
+ error = self.assertRaises(TypeError, ObjectSet, 123)
+ self.assertThat(str(error), Equals("data must be sequence-like, not int"))
+
+ def test__length_is_number_of_items(self):
+ items = [0] * randrange(0, 100)
+ objectset = ObjectSet(items)
+ self.assertThat(objectset, HasLength(len(items)))
+
+ def test__can_be_indexed(self):
+ items = [make_name_without_spaces(str(index)) for index in range(5)]
+ objectset = ObjectSet(items)
+ for index, item in enumerate(items):
+ self.assertThat(objectset[index], Equals(item))
+
+ def test__can_be_sliced(self):
+ items = [make_name_without_spaces(str(index)) for index in range(5)]
+ objectset1 = ObjectSet(items)
+ objectset2 = objectset1[1:3]
+ self.assertThat(objectset2, IsInstance(ObjectSet))
+ self.assertThat(list(objectset2), Equals(items[1:3]))
+
+ def test__iteration_yield_items(self):
+ items = [make_name_without_spaces(str(index)) for index in range(5)]
+ objectset = ObjectSet(items)
+ self.assertThat(list(objectset), Equals(items))
+
+ def test__reversed_yields_items_in_reverse(self):
+ items = [make_name_without_spaces(str(index)) for index in range(5)]
+ objectset = ObjectSet(items)
+ self.assertThat(list(reversed(objectset)), Equals(items[::-1]))
+
+ def test__membership_can_be_tested(self):
+ item1 = make_name_without_spaces("item")
+ item2 = make_name_without_spaces("item")
+ objectset = ObjectSet([item1])
+ self.assertThat(objectset, Contains(item1))
+ self.assertThat(objectset, Not(Contains(item2)))
+
+ def test__equal_when_items_match(self):
+ items = [{"key": make_name("value")}]
+ objectset_a = ObjectSet(items)
+ objectset_b = ObjectSet(items)
+ self.assertThat(objectset_a, Equals(objectset_b))
+ self.assertThat(objectset_b, Equals(objectset_a))
+
+ def test__not_equal_when_types_different(self):
+ # Even if one is a subclass of the other.
+ items = [{"key": make_name("value")}]
+ objectset_a = ObjectSet(items)
+ objectset_b = type("ObjectSet", (ObjectSet,), {})(items)
+ self.assertThat(objectset_a, Not(Equals(objectset_b)))
+ self.assertThat(objectset_b, Not(Equals(objectset_a)))
+
+ def test__string_representation_includes_length_and_items(self):
+ class Example(Object):
+ alice = ObjectField("alice")
+
+ class ExampleSet(ObjectSet):
+ pass
+
+ example = ExampleSet(
+ [Example({"alice": "wonderland"}), Example({"alice": "cooper"})]
+ )
+
+ self.assertThat(
+ repr(example),
+ Equals(
+ ", "
+ "]>"
+ ),
+ )
+
+
+class TestObjectField(TestCase):
+ """Tests for `ObjectField`."""
+
+ def test__gets_sets_and_deletes_the_given_name_from_object(self):
+ class Example(Object):
+ alice = ObjectField("alice")
+
+ example = Example({})
+
+ # At first, referencing "alice" yields an exception.
+ self.assertRaises(AttributeError, getattr, example, "alice")
+ self.assertThat(example._data, Equals({}))
+
+ # Setting "alice" stores the value in the object's _data dict.
+ example.alice = sentinel.alice
+ self.assertThat(example.alice, Is(sentinel.alice))
+ self.assertThat(example._data, Equals({"alice": sentinel.alice}))
+
+ # Deleting "alice" removes the value from the object's _data dict, and
+ # referencing "alice" yields an exception.
+ del example.alice
+ self.assertRaises(AttributeError, getattr, example, "alice")
+ self.assertThat(example._data, Equals({}))
+
+ def test__default_is_returned_when_value_not_found_in_object(self):
+ class Example(Object):
+ alice = ObjectField("alice", default=sentinel.alice_default)
+
+ example = Example({})
+
+ # At first, referencing "alice" yields the default value.
+ self.assertThat(example.alice, Is(sentinel.alice_default))
+ self.assertThat(example._data, Equals({}))
+
+ # Setting "alice" stores the value in the object's _data dict.
+ example.alice = sentinel.alice
+ self.assertThat(example, MatchesStructure(alice=Is(sentinel.alice)))
+ self.assertThat(example._data, Equals({"alice": sentinel.alice}))
+
+ # Deleting "alice" removes the value from the object's _data dict, and
+ # referencing "alice" again yields the default value.
+ del example.alice
+ self.assertThat(example.alice, Is(sentinel.alice_default))
+ self.assertThat(example._data, Equals({}))
+
+ def test__readonly_prevents_setting_or_deleting(self):
+ class Example(Object):
+ alice = ObjectField("alice", readonly=True)
+
+ example = Example({"alice": sentinel.in_wonderland})
+
+ self.assertThat(example.alice, Is(sentinel.in_wonderland))
+ self.assertRaises(AttributeError, setattr, example, "alice", 123)
+ self.assertThat(example.alice, Is(sentinel.in_wonderland))
+ self.assertRaises(AttributeError, delattr, example, "alice")
+ self.assertThat(example.alice, Is(sentinel.in_wonderland))
+
+ def test__conversion_and_validation_happens(self):
+ class AliceField(ObjectField):
+ """A most peculiar field."""
+
+ def datum_to_value(self, instance, datum):
+ return datum + instance.datum_to_value_delta
+
+ def value_to_datum(self, instance, value):
+ return value + instance.value_to_datum_delta
+
+ class Example(Object):
+ alice = AliceField("alice")
+ # Deltas to apply to datums and values.
+ datum_to_value_delta = 2
+ value_to_datum_delta = 3
+
+ example = Example({})
+ example.alice = 0
+
+ # value_to_datum_delta was added to the value we specified before
+ # being stored in the object's _data dict.
+ self.assertThat(example._data, Equals({"alice": 3}))
+
+ # datum_to_value_delta is added to the datum in the object's _data
+ # dict before being returned to us.
+ self.assertThat(example.alice, Equals(5))
+
+ def test__default_is_not_subject_to_conversion_or_validation(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice", default=sentinel.alice_default)
+
+ example = Example({})
+
+ # The default given is treated as a Python-side value rather than a
+ # MAAS-side datum, so is not passed through datum_to_value (or
+ # value_to_datum for that matter).
+ self.assertThat(example.alice, Is(sentinel.alice_default))
+
+ def test__set_new_value_is_set_in_changed(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ example = Example({})
+
+ example.alice = sentinel.alice
+ self.assertThat(example._changed_data, Equals({"alice": sentinel.alice}))
+
+ def test__set_update_value_is_set_in_changed(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ example = Example({"alice": sentinel.alice})
+
+ example.alice = sentinel.new_alice
+ self.assertThat(example._changed_data, Equals({"alice": sentinel.new_alice}))
+
+ def test__set_update_value_replaces_changed(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ example = Example({"alice": sentinel.alice})
+
+ example.alice = sentinel.new_alice
+ self.assertThat(example._changed_data, Equals({"alice": sentinel.new_alice}))
+ example.alice = sentinel.newer_alice
+ self.assertThat(example._changed_data, Equals({"alice": sentinel.newer_alice}))
+
+ def test__set_update_value_to_orig_removes_from_changed(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ alice = make_name_without_spaces("alice")
+ new_alice = make_name_without_spaces("alice")
+ example = Example({"alice": alice})
+
+ example.alice = new_alice
+ self.assertThat(example._changed_data, Equals({"alice": new_alice}))
+ example.alice = alice
+ self.assertThat(example._changed_data, Equals({}))
+
+ def test__delete_marks_field_deleted(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ example = Example({"alice": sentinel.alice})
+
+ del example.alice
+ self.assertThat(example._changed_data, Equals({"alice": None}))
+
+ def test__set_deleted_field_sets_changed(self):
+ class AliceField(ObjectField):
+ """Another most peculiar field."""
+
+ class Example(Object):
+ alice = AliceField("alice")
+
+ alice = make_name_without_spaces("alice")
+ new_alice = make_name_without_spaces("alice")
+ example = Example({"alice": alice})
+
+ del example.alice
+ example.alice = new_alice
+ self.assertThat(example._changed_data, Equals({"alice": new_alice}))
+ example.alice = alice
+ self.assertThat(example._changed_data, Equals({}))
+
+
+class TestObjectFieldChecked(TestCase):
+ """Tests for `ObjectField.Checked`."""
+
+ def test__creates_subclass(self):
+ field = ObjectField.Checked("alice")
+ self.assertThat(type(field).__mro__, Contains(ObjectField))
+ self.assertThat(type(field), Not(Is(ObjectField)))
+ self.assertThat(type(field).__name__, Equals("ObjectField.Checked#alice"))
+
+ def test__overrides_datum_to_value(self):
+ add_one = lambda value: value + 1
+ field = ObjectField.Checked("alice", datum_to_value=add_one)
+ self.assertThat(field.datum_to_value(None, 1), Equals(2))
+
+ def test__overrides_value_to_daturm(self):
+ add_one = lambda value: value + 1
+ field = ObjectField.Checked("alice", value_to_datum=add_one)
+ self.assertThat(field.value_to_datum(None, 1), Equals(2))
+
+ def test__works_in_place(self):
+
+ # Deltas to apply to datums and values.
+ datum_to_value_delta = 2
+ value_to_datum_delta = 3
+
+ class Example(Object):
+ alice = ObjectField.Checked(
+ "alice",
+ (lambda datum: datum + datum_to_value_delta),
+ (lambda value: value + value_to_datum_delta),
+ )
+
+ example = Example({})
+ example.alice = 0
+
+ # value_to_datum_delta was added to the value we specified before
+ # being stored in the object's _data dict.
+ self.assertThat(example._data, Equals({"alice": 3}))
+
+ # datum_to_value_delta is added to the datum in the object's _data
+ # dict before being returned to us.
+ self.assertThat(example.alice, Equals(5))
+
+
+class TestObjectFieldRelated(TestCase):
+ """Tests for `ObjectFieldRelated`."""
+
+ def test__init__requires_str_or_Object_class(self):
+ self.assertRaises(TypeError, ObjectFieldRelated, "name", 0)
+ self.assertRaises(TypeError, ObjectFieldRelated, "name", object)
+ # Doesn't raise error.
+ ObjectFieldRelated("name", "class")
+ ObjectFieldRelated("name", Object)
+
+ def test_datum_to_value_returns_None_on_None(self):
+ self.assertIsNone(
+ ObjectFieldRelated("name", "class").datum_to_value(object(), None)
+ )
+
+ def test_datum_to_value_converts_to_bound_class(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("InstObject", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject")
+ rel_id = randint(0, 20)
+ rel_object = field.datum_to_value(instance, rel_id)
+ self.assertIsInstance(rel_object, rel_object_type)
+ self.assertFalse(rel_object.loaded)
+ self.assertThat(
+ rel_object._data, Equals({"instobject": instance, "pk_d": rel_id})
+ )
+
+ def test_datum_to_value_uses_reverse_name(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject", reverse="reverse")
+ rel_id = randint(0, 20)
+ rel_object = field.datum_to_value(instance, rel_id)
+ self.assertIsInstance(rel_object, rel_object_type)
+ self.assertFalse(rel_object.loaded)
+ self.assertThat(rel_object._data, Equals({"reverse": instance, "pk_d": rel_id}))
+
+ def test_datum_to_value_doesnt_include_reverse(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject", reverse=None)
+ rel_id = randint(0, 20)
+ rel_object = field.datum_to_value(instance, rel_id)
+ self.assertIsInstance(rel_object, rel_object_type)
+ self.assertFalse(rel_object.loaded)
+ self.assertThat(rel_object._data, Equals({"pk_d": rel_id}))
+
+ def test_value_to_datum_returns_None_on_None(self):
+ self.assertIsNone(
+ ObjectFieldRelated("name", "class").value_to_datum(object(), None)
+ )
+
+ def test_value_to_datum_requires_same_class(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject")
+ error = self.assertRaises(TypeError, field.value_to_datum, instance, object())
+ self.assertThat(str(error), Equals("must be RelObject, not object"))
+
+ def test_value_to_datum_with_one_primary_key(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject")
+ rel_id = randint(0, 20)
+ rel_object = rel_object_type(rel_id)
+ self.assertThat(field.value_to_datum(instance, rel_object), Equals(rel_id))
+
+ def test_value_to_datum_with_multiple_primary_keys(self):
+ rel_object_type = type(
+ "RelObject",
+ (Object,),
+ {"pk1": ObjectField("pk_1", pk=0), "pk2": ObjectField("pk_2", pk=1)},
+ )
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related", "RelObject")
+ rel_1 = randint(0, 20)
+ rel_2 = randint(0, 20)
+ rel_object = rel_object_type((rel_1, rel_2))
+ self.assertThat(
+ field.value_to_datum(instance, rel_object), Equals((rel_1, rel_2))
+ )
+
+ def test_value_to_datum_raises_error_when_no_primary_keys(self):
+ rel_object_type = type("RelObject", (Object,), {"pk": ObjectField("pk_d")})
+ origin = Mock()
+ origin.RelObject = rel_object_type
+ instance = type("Object", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelated("related_id", "RelObject")
+ rel_id = randint(0, 20)
+ rel_object = rel_object_type({"pk_d": rel_id})
+ error = self.assertRaises(
+ AttributeError, field.value_to_datum, instance, rel_object
+ )
+ self.assertThat(
+ str(error),
+ Equals(
+ "unable to perform set object no primary key "
+ "fields defined for RelObject"
+ ),
+ )
+
+
+class TestObjectFieldRelatedSet(TestCase):
+ """Tests for `ObjectFieldRelatedSet`."""
+
+ def test__init__requires_str_or_Object_class(self):
+ self.assertRaises(TypeError, ObjectFieldRelatedSet, "name", 0)
+ self.assertRaises(TypeError, ObjectFieldRelatedSet, "name", object)
+ # Doesn't raise error.
+ ObjectFieldRelatedSet("name", "class")
+ ObjectFieldRelatedSet("name", ObjectSet)
+
+ def test_datum_to_value_returns_empty_list_on_None(self):
+ self.assertEquals(
+ ObjectFieldRelatedSet("name", "class").datum_to_value(object(), None), []
+ )
+
+ def test_datum_must_be_a_sequence(self):
+ field = ObjectFieldRelatedSet("name", "class")
+ error = self.assertRaises(TypeError, field.datum_to_value, object(), 0)
+ self.assertThat(str(error), Equals("datum must be a sequence, not int"))
+
+ def test_datum_to_value_converts_to_set_of_bound_class(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ rel_object_set_type = type(
+ "RelObjectSet", (ObjectSet,), {"_object": rel_object_type}
+ )
+ origin = Mock()
+ origin.RelObjectSet = rel_object_set_type
+ instance = type("InstObject", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelatedSet("related_ids", "RelObjectSet")
+ rel_ids = range(5)
+ rel_object_set = field.datum_to_value(instance, rel_ids)
+ self.assertEquals(5, len(rel_object_set))
+ self.assertIsInstance(rel_object_set[0], rel_object_type)
+ self.assertFalse(rel_object_set[0].loaded)
+ self.assertThat(
+ rel_object_set[0]._data,
+ Equals({"instobject": instance, "pk_d": rel_ids[0]}),
+ )
+
+ def test_datum_to_value_uses_reverse_name(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ rel_object_set_type = type(
+ "RelObjectSet", (ObjectSet,), {"_object": rel_object_type}
+ )
+ origin = Mock()
+ origin.RelObjectSet = rel_object_set_type
+ instance = type("InstObject", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelatedSet("related_ids", "RelObjectSet", reverse="reverse")
+ rel_ids = range(5)
+ rel_object_set = field.datum_to_value(instance, rel_ids)
+ self.assertEquals(5, len(rel_object_set))
+ self.assertIsInstance(rel_object_set[0], rel_object_type)
+ self.assertFalse(rel_object_set[0].loaded)
+ self.assertThat(
+ rel_object_set[0]._data, Equals({"reverse": instance, "pk_d": rel_ids[0]})
+ )
+
+ def test_datum_to_value_doesnt_include_reverse(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ rel_object_set_type = type(
+ "RelObjectSet", (ObjectSet,), {"_object": rel_object_type}
+ )
+ origin = Mock()
+ origin.RelObjectSet = rel_object_set_type
+ instance = type("InstObject", (Object,), {"_origin": origin})({})
+ field = ObjectFieldRelatedSet("related_ids", "RelObjectSet", reverse=None)
+ rel_ids = range(5)
+ rel_object_set = field.datum_to_value(instance, rel_ids)
+ self.assertEquals(5, len(rel_object_set))
+ self.assertIsInstance(rel_object_set[0], rel_object_type)
+ self.assertFalse(rel_object_set[0].loaded)
+ self.assertThat(rel_object_set[0]._data, Equals({"pk_d": rel_ids[0]}))
+
+ def test_datum_to_value_wraps_managed_create(self):
+ rel_object_type = type(
+ "RelObject", (Object,), {"pk": ObjectField("pk_d", pk=True)}
+ )
+ create_mock = AsyncCallableMock(return_value=rel_object_type({}))
+ rel_object_set_type = type(
+ "RelObjectSet",
+ (ObjectSet,),
+ {"_object": rel_object_type, "create": create_mock},
+ )
+ origin = Mock()
+ origin.RelObjectSet = rel_object_set_type
+ instance = type("InstObject", (Object,), {"_origin": origin})(
+ {"related_ids": []}
+ )
+ field = ObjectFieldRelatedSet("related_ids", "RelObjectSet")
+ rel_ids = range(5)
+ rel_object_set = field.datum_to_value(instance, rel_ids)
+ rel_object_set.create()
+ self.assertEquals(
+ "RelObjectSet.Managed#InstObject", type(rel_object_set).__name__
+ )
+ self.assertThat(create_mock.call_args_list, Equals([call(instance)]))
+
+
+class TestOriginBase(TestCase):
+ """Tests for `OriginBase`."""
+
+ def test__session_is_underlying_session(self):
+ profile = make_profile()
+ session = bones.SessionAPI.fromProfile(profile)
+ origin = OriginBase(session)
+ self.assertThat(origin.session, Is(session))
+
+ def test__session_is_read_only(self):
+ profile = make_profile()
+ session = bones.SessionAPI.fromProfile(profile)
+ origin = OriginBase(session)
+ self.assertRaises(AttributeError, setattr, origin, "session", None)
diff --git a/maas/client/viscera/tests/test_account.py b/maas/client/viscera/tests/test_account.py
new file mode 100644
index 00000000..26bd6c93
--- /dev/null
+++ b/maas/client/viscera/tests/test_account.py
@@ -0,0 +1,74 @@
+"""Test for `maas.client.viscera.account`."""
+
+from testtools.matchers import Equals, IsInstance, MatchesAll
+
+from .. import account
+from ...testing import make_name_without_spaces, TestCase
+from ...utils import creds
+from ...utils.testing import make_Credentials
+from ..testing import bind
+
+
+def make_origin():
+ # Create a new origin with Account.
+ return bind(account.Account)
+
+
+class TestAccount(TestCase):
+ def test__create_credentials_returns_Credentials(self):
+ consumer_key = (make_name_without_spaces("consumer_key"),)
+ token_key = (make_name_without_spaces("token_key"),)
+ token_secret = (make_name_without_spaces("token_secret"),)
+
+ origin = make_origin()
+ create_authorisation_token = origin.Account._handler.create_authorisation_token
+ create_authorisation_token.return_value = {
+ "consumer_key": consumer_key,
+ "token_key": token_key,
+ "token_secret": token_secret,
+ }
+
+ credentials = origin.Account.create_credentials()
+ self.assertThat(
+ credentials,
+ MatchesAll(
+ IsInstance(creds.Credentials),
+ Equals((consumer_key, token_key, token_secret)),
+ ),
+ )
+
+ def test__create_credentials_ignores_other_keys_in_response(self):
+ consumer_key = (make_name_without_spaces("consumer_key"),)
+ token_key = (make_name_without_spaces("token_key"),)
+ token_secret = (make_name_without_spaces("token_secret"),)
+
+ origin = make_origin()
+ create_authorisation_token = origin.Account._handler.create_authorisation_token
+ create_authorisation_token.return_value = {
+ "name": "cookie-monster",
+ "fur-colour": "blue",
+ "consumer_key": consumer_key,
+ "token_key": token_key,
+ "token_secret": token_secret,
+ }
+
+ credentials = origin.Account.create_credentials()
+ self.assertThat(
+ credentials,
+ MatchesAll(
+ IsInstance(creds.Credentials),
+ Equals((consumer_key, token_key, token_secret)),
+ ),
+ )
+
+ def test__delete_credentials_sends_token_key(self):
+ origin = make_origin()
+ delete_authorisation_token = origin.Account._handler.delete_authorisation_token
+ delete_authorisation_token.return_value = None
+ credentials = make_Credentials()
+
+ origin.Account.delete_credentials(credentials)
+
+ delete_authorisation_token.assert_called_once_with(
+ token_key=credentials.token_key
+ )
diff --git a/maas/client/viscera/tests/test_bcache_cache_sets.py b/maas/client/viscera/tests/test_bcache_cache_sets.py
new file mode 100644
index 00000000..f7bba667
--- /dev/null
+++ b/maas/client/viscera/tests/test_bcache_cache_sets.py
@@ -0,0 +1,200 @@
+"""Test for `maas.client.viscera.bcache_cache_sets`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..bcache_cache_sets import BcacheCacheSet, BcacheCacheSets
+from ..block_devices import BlockDevice, BlockDevices
+from ..nodes import Node, Nodes
+from ..partitions import Partition, Partitions
+
+from ..testing import bind
+from ...enum import BlockDeviceType
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with required objects.
+ """
+ return bind(
+ BcacheCacheSets,
+ BcacheCacheSet,
+ BlockDevices,
+ BlockDevice,
+ Partitions,
+ Partition,
+ Nodes,
+ Node,
+ )
+
+
+class TestBcacheCacheSets(TestCase):
+ def test__read_bad_node_type(self):
+ BcacheCacheSets = make_origin().BcacheCacheSets
+ error = self.assertRaises(
+ TypeError, BcacheCacheSets.read, random.randint(0, 100)
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ BcacheCacheSets = make_origin().BcacheCacheSets
+ system_id = make_string_without_spaces()
+ cache_sets = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "cache_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ for _ in range(3)
+ ]
+ BcacheCacheSets._handler.read.return_value = cache_sets
+ cache_sets = BcacheCacheSets.read(system_id)
+ self.assertThat(len(cache_sets), Equals(3))
+ BcacheCacheSets._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ BcacheCacheSets, Node = origin.BcacheCacheSets, origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ cache_sets = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "cache_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ for _ in range(3)
+ ]
+ BcacheCacheSets._handler.read.return_value = cache_sets
+ cache_sets = BcacheCacheSets.read(node)
+ self.assertThat(len(cache_sets), Equals(3))
+ BcacheCacheSets._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__create_bad_node_type(self):
+ origin = make_origin()
+ BcacheCacheSets = origin.BcacheCacheSets
+ error = self.assertRaises(
+ TypeError,
+ BcacheCacheSets.create,
+ random.randint(0, 100),
+ origin.BlockDevice({}),
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__create_bad_cache_device_type(self):
+ BcacheCacheSets = make_origin().BcacheCacheSets
+ error = self.assertRaises(
+ TypeError,
+ BcacheCacheSets.create,
+ make_string_without_spaces(),
+ random.randint(0, 100),
+ )
+ self.assertEquals(
+ "cache_device must be a BlockDevice or Partition, not int", str(error)
+ )
+
+ def test__create_with_block_device(self):
+ origin = make_origin()
+ BcacheCacheSets = origin.BcacheCacheSets
+ BlockDevice = origin.BlockDevice
+ block_device = BlockDevice({"id": random.randint(0, 100)})
+ BcacheCacheSets._handler.create.return_value = {
+ "id": random.randint(0, 100),
+ "cache_device": block_device,
+ }
+ system_id = make_string_without_spaces()
+ BcacheCacheSets.create(system_id, block_device)
+ BcacheCacheSets._handler.create.assert_called_once_with(
+ system_id=system_id, cache_device=block_device.id
+ )
+
+ def test__create_with_partition(self):
+ origin = make_origin()
+ BcacheCacheSets = origin.BcacheCacheSets
+ Partition = origin.Partition
+ partition = Partition({"id": random.randint(0, 100)})
+ BcacheCacheSets._handler.create.return_value = {
+ "id": random.randint(0, 100),
+ "cache_device": partition,
+ }
+ system_id = make_string_without_spaces()
+ BcacheCacheSets.create(system_id, partition)
+ BcacheCacheSets._handler.create.assert_called_once_with(
+ system_id=system_id, cache_partition=partition.id
+ )
+
+
+class TestBcacheCacheSet(TestCase):
+ def test__read_bad_node_type(self):
+ BcacheCacheSet = make_origin().BcacheCacheSet
+ error = self.assertRaises(
+ TypeError,
+ BcacheCacheSet.read,
+ random.randint(0, 100),
+ random.randint(0, 100),
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ BcacheCacheSet = make_origin().BcacheCacheSet
+ system_id = make_string_without_spaces()
+ cache_set = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "cache_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ BcacheCacheSet._handler.read.return_value = cache_set
+ BcacheCacheSet.read(system_id, cache_set["id"])
+ BcacheCacheSet._handler.read.assert_called_once_with(
+ system_id=system_id, id=cache_set["id"]
+ )
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ BcacheCacheSet = origin.BcacheCacheSet
+ Node = origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ cache_set = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "cache_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ BcacheCacheSet._handler.read.return_value = cache_set
+ BcacheCacheSet.read(node, cache_set["id"])
+ BcacheCacheSet._handler.read.assert_called_once_with(
+ system_id=system_id, id=cache_set["id"]
+ )
+
+ def test__delete(self):
+ BcacheCacheSet = make_origin().BcacheCacheSet
+ system_id = make_string_without_spaces()
+ cache_set = BcacheCacheSet(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "cache_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ )
+ BcacheCacheSet._handler.delete.return_value = None
+ cache_set.delete()
+ BcacheCacheSet._handler.delete.assert_called_once_with(
+ system_id=system_id, id=cache_set.id
+ )
diff --git a/maas/client/viscera/tests/test_bcaches.py b/maas/client/viscera/tests/test_bcaches.py
new file mode 100644
index 00000000..10271ce9
--- /dev/null
+++ b/maas/client/viscera/tests/test_bcaches.py
@@ -0,0 +1,316 @@
+"""Test for `maas.client.viscera.bcache_cache_sets`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..bcache_cache_sets import BcacheCacheSet, BcacheCacheSets
+from ..bcaches import Bcache, Bcaches
+from ..block_devices import BlockDevice, BlockDevices
+from ..nodes import Node, Nodes
+from ..partitions import Partition, Partitions
+
+from ..testing import bind
+from ...enum import BlockDeviceType, CacheMode
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with required objects.
+ """
+ return bind(
+ Bcaches,
+ Bcache,
+ BcacheCacheSets,
+ BcacheCacheSet,
+ BlockDevices,
+ BlockDevice,
+ Partitions,
+ Partition,
+ Nodes,
+ Node,
+ )
+
+
+class TestBcaches(TestCase):
+ def test__by_name(self):
+ origin = make_origin()
+ Bcaches, Bcache = origin.Bcaches, origin.Bcache
+ system_id = make_string_without_spaces()
+ bcache_names = [make_string_without_spaces() for _ in range(3)]
+ bcaches_by_name = {
+ name: Bcache(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": name,
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ "cache_set": {"id": random.randint(0, 100)},
+ "virtual_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.VIRTUAL.value,
+ },
+ }
+ )
+ for name in bcache_names
+ }
+ bcaches = Bcaches([obj for _, obj in bcaches_by_name.items()])
+ self.assertEqual(bcaches_by_name, bcaches.by_name)
+
+ def test__get_by_name(self):
+ origin = make_origin()
+ Bcaches, Bcache = origin.Bcaches, origin.Bcache
+ system_id = make_string_without_spaces()
+ bcache_names = [make_string_without_spaces() for _ in range(3)]
+ bcaches_by_name = {
+ name: Bcache(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": name,
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ "cache_set": {"id": random.randint(0, 100)},
+ "virtual_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.VIRTUAL.value,
+ },
+ }
+ )
+ for name in bcache_names
+ }
+ bcaches = Bcaches([obj for _, obj in bcaches_by_name.items()])
+ name = bcache_names[0]
+ self.assertEqual(bcaches_by_name[name], bcaches.get_by_name(name))
+
+ def test__read_bad_node_type(self):
+ Bcaches = make_origin().Bcaches
+ error = self.assertRaises(TypeError, Bcaches.read, random.randint(0, 100))
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ Bcaches = make_origin().Bcaches
+ system_id = make_string_without_spaces()
+ bcaches = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ "cache_set": {"id": random.randint(0, 100)},
+ "virtual_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.VIRTUAL.value,
+ },
+ }
+ for _ in range(3)
+ ]
+ Bcaches._handler.read.return_value = bcaches
+ bcaches = Bcaches.read(system_id)
+ self.assertThat(len(bcaches), Equals(3))
+ Bcaches._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ Bcaches, Node = origin.Bcaches, origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ bcaches = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ "cache_set": {"id": random.randint(0, 100)},
+ "virtual_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.VIRTUAL.value,
+ },
+ }
+ for _ in range(3)
+ ]
+ Bcaches._handler.read.return_value = bcaches
+ bcaches = Bcaches.read(node)
+ self.assertThat(len(bcaches), Equals(3))
+ Bcaches._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__create_bad_node_type(self):
+ origin = make_origin()
+ Bcaches = origin.Bcaches
+ error = self.assertRaises(
+ TypeError,
+ Bcaches.create,
+ random.randint(0, 100),
+ make_string_without_spaces(),
+ origin.BlockDevice({}),
+ random.randint(0, 100),
+ CacheMode.WRITETHROUGH,
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__create_bad_cache_device_type(self):
+ Bcaches = make_origin().Bcaches
+ error = self.assertRaises(
+ TypeError,
+ Bcaches.create,
+ make_string_without_spaces(),
+ make_string_without_spaces(),
+ random.randint(0, 100),
+ random.randint(0, 100),
+ CacheMode.WRITETHROUGH,
+ )
+ self.assertEquals(
+ "backing_device must be a BlockDevice or Partition, not int", str(error)
+ )
+
+ def test__create_bad_cache_set_type(self):
+ origin = make_origin()
+ Bcaches = origin.Bcaches
+ error = self.assertRaises(
+ TypeError,
+ Bcaches.create,
+ make_string_without_spaces(),
+ make_string_without_spaces(),
+ origin.BlockDevice({"id": random.randint(0, 100)}),
+ make_string_without_spaces(),
+ CacheMode.WRITETHROUGH,
+ )
+ self.assertEquals(
+ "cache_set must be a BcacheCacheSet or int, not str", str(error)
+ )
+
+ def test__create_bad_cache_mode_type(self):
+ origin = make_origin()
+ Bcaches = origin.Bcaches
+ error = self.assertRaises(
+ TypeError,
+ Bcaches.create,
+ make_string_without_spaces(),
+ make_string_without_spaces(),
+ origin.BlockDevice({"id": random.randint(0, 100)}),
+ random.randint(0, 100),
+ "writethrough",
+ )
+ self.assertEquals("cache_mode must be a CacheMode, not str", str(error))
+
+ def test__create_with_block_device(self):
+ origin = make_origin()
+ Bcaches = origin.Bcaches
+ BlockDevice = origin.BlockDevice
+ BcacheCacheSet = origin.BcacheCacheSet
+ block_device = BlockDevice({"id": random.randint(0, 100)})
+ cache_set = BcacheCacheSet({"id": random.randint(0, 100)})
+ Bcaches._handler.create.return_value = {
+ "id": random.randint(0, 100),
+ "backing_device": block_device._data,
+ "cache_set": cache_set._data,
+ }
+ name = make_string_without_spaces()
+ system_id = make_string_without_spaces()
+ Bcaches.create(system_id, name, block_device, cache_set, CacheMode.WRITEBACK)
+ Bcaches._handler.create.assert_called_once_with(
+ system_id=system_id,
+ name=name,
+ backing_device=block_device.id,
+ cache_set=cache_set.id,
+ cache_mode=CacheMode.WRITEBACK.value,
+ )
+
+ def test__create_with_partition(self):
+ origin = make_origin()
+ Bcaches = origin.Bcaches
+ Partition = origin.Partition
+ partition = Partition({"id": random.randint(0, 100)})
+ cache_set_id = random.randint(0, 100)
+ Bcaches._handler.create.return_value = {
+ "id": random.randint(0, 100),
+ "backing_device": partition._data,
+ "cache_set": {"id": cache_set_id},
+ }
+ name = make_string_without_spaces()
+ system_id = make_string_without_spaces()
+ Bcaches.create(system_id, name, partition, cache_set_id, CacheMode.WRITEBACK)
+ Bcaches._handler.create.assert_called_once_with(
+ system_id=system_id,
+ name=name,
+ backing_partition=partition.id,
+ cache_set=cache_set_id,
+ cache_mode=CacheMode.WRITEBACK.value,
+ )
+
+
+class TestBcache(TestCase):
+ def test__read_bad_node_type(self):
+ Bcache = make_origin().Bcache
+ error = self.assertRaises(
+ TypeError, Bcache.read, random.randint(0, 100), random.randint(0, 100)
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ Bcache = make_origin().Bcache
+ system_id = make_string_without_spaces()
+ bcache = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ Bcache._handler.read.return_value = bcache
+ Bcache.read(system_id, bcache["id"])
+ Bcache._handler.read.assert_called_once_with(
+ system_id=system_id, id=bcache["id"]
+ )
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ Bcache = origin.Bcache
+ Node = origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ bcache = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ Bcache._handler.read.return_value = bcache
+ Bcache.read(node, bcache["id"])
+ Bcache._handler.read.assert_called_once_with(
+ system_id=system_id, id=bcache["id"]
+ )
+
+ def test__delete(self):
+ Bcache = make_origin().Bcache
+ system_id = make_string_without_spaces()
+ bcache = Bcache(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "backing_device": {
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ },
+ }
+ )
+ Bcache._handler.delete.return_value = None
+ bcache.delete()
+ Bcache._handler.delete.assert_called_once_with(
+ system_id=system_id, id=bcache.id
+ )
diff --git a/maas/client/viscera/tests/test_block_devices.py b/maas/client/viscera/tests/test_block_devices.py
new file mode 100644
index 00000000..3775a2c3
--- /dev/null
+++ b/maas/client/viscera/tests/test_block_devices.py
@@ -0,0 +1,481 @@
+"""Test for `maas.client.viscera.block_devices`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..block_devices import BlockDevice, BlockDevices
+from ..nodes import Node, Nodes
+from ..partitions import Partition, Partitions
+
+from ..testing import bind
+from ...enum import BlockDeviceType
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with required objects.
+ """
+ return bind(BlockDevices, BlockDevice, Partitions, Partition, Nodes, Node)
+
+
+class TestBlockDevices(TestCase):
+ def test__by_name(self):
+ origin = make_origin()
+ BlockDevices, BlockDevice = origin.BlockDevices, origin.BlockDevice
+ system_id = make_string_without_spaces()
+ block_names = [make_string_without_spaces() for _ in range(3)]
+ blocks_by_name = {
+ name: BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": name,
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ )
+ for name in block_names
+ }
+ blocks = BlockDevices([obj for _, obj in blocks_by_name.items()])
+ self.assertEqual(blocks_by_name, blocks.by_name)
+
+ def test__get_by_name(self):
+ origin = make_origin()
+ BlockDevices, BlockDevice = origin.BlockDevices, origin.BlockDevice
+ system_id = make_string_without_spaces()
+ block_names = [make_string_without_spaces() for _ in range(3)]
+ blocks_by_name = {
+ name: BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": name,
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ )
+ for name in block_names
+ }
+ bcaches = BlockDevices([obj for _, obj in blocks_by_name.items()])
+ name = block_names[0]
+ self.assertEqual(blocks_by_name[name], bcaches.get_by_name(name))
+
+ def test__read_bad_node_type(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(TypeError, BlockDevices.read, random.randint(0, 100))
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ BlockDevices = make_origin().BlockDevices
+ system_id = make_string_without_spaces()
+ blocks = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ for _ in range(3)
+ ]
+ BlockDevices._handler.read.return_value = blocks
+ blocks = BlockDevices.read(system_id)
+ self.assertThat(len(blocks), Equals(3))
+ BlockDevices._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ BlockDevices, Node = origin.BlockDevices, origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ blocks = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ for _ in range(3)
+ ]
+ BlockDevices._handler.read.return_value = blocks
+ blocks = BlockDevices.read(node)
+ self.assertThat(len(blocks), Equals(3))
+ BlockDevices._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__create_bad_node_type(self):
+ origin = make_origin()
+ BlockDevices = origin.BlockDevices
+ error = self.assertRaises(
+ TypeError,
+ BlockDevices.create,
+ random.randint(0, 100),
+ "sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=(4096 * 1024),
+ block_size=512,
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__create_missing_size(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ model="QEMU",
+ serial="QEMU0001",
+ block_size=512,
+ )
+ self.assertEquals("size must be provided and greater than zero.", str(error))
+
+ def test__create_negative_size(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=-1,
+ block_size=512,
+ )
+ self.assertEquals("size must be provided and greater than zero.", str(error))
+
+ def test__create_missing_block_size(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=(4096 * 1024),
+ block_size=None,
+ )
+ self.assertEquals(
+ "block_size must be provided and greater than zero.", str(error)
+ )
+
+ def test__create_negative_block_size(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=(4096 * 1024),
+ block_size=-1,
+ )
+ self.assertEquals(
+ "block_size must be provided and greater than zero.", str(error)
+ )
+
+ def test__create_model_requires_serial(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ model="QEMU",
+ size=(4096 * 1024),
+ )
+ self.assertEquals("serial must be provided when model is provided.", str(error))
+
+ def test__create_serial_requires_model(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ serial="QEMU0001",
+ size=(4096 * 1024),
+ )
+ self.assertEquals("model must be provided when serial is provided.", str(error))
+
+ def test__create_requires_model_and_serial_or_id_path(self):
+ BlockDevices = make_origin().BlockDevices
+ error = self.assertRaises(
+ ValueError,
+ BlockDevices.create,
+ make_string_without_spaces(),
+ "sda",
+ size=(4096 * 1024),
+ )
+ self.assertEquals(
+ "Either model/serial is provided or id_path must be provided.", str(error)
+ )
+
+ def test__create(self):
+ origin = make_origin()
+ BlockDevices = origin.BlockDevices
+ system_id = make_string_without_spaces()
+ size = 4096 * 1024
+ BlockDevices._handler.create.return_value = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": "sda",
+ "model": "QEMU",
+ "serial": "QEMU0001",
+ "size": size,
+ "block_size": 512,
+ }
+ BlockDevices.create(
+ system_id, "sda", model="QEMU", serial="QEMU0001", size=size
+ )
+ BlockDevices._handler.create.assert_called_once_with(
+ system_id=system_id,
+ name="sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=size,
+ block_size=512,
+ )
+
+ def test__create_with_tags(self):
+ origin = make_origin()
+ BlockDevices, BlockDevice = origin.BlockDevices, origin.BlockDevice
+ system_id = make_string_without_spaces()
+ block_id = random.randint(0, 100)
+ size = 4096 * 1024
+ BlockDevices._handler.create.return_value = {
+ "system_id": system_id,
+ "id": block_id,
+ "name": "sda",
+ "model": "QEMU",
+ "serial": "QEMU0001",
+ "size": size,
+ "block_size": 512,
+ "tags": [],
+ }
+ BlockDevices._handler.add_tag.return_value = None
+ tag = make_string_without_spaces()
+ BlockDevices.create(
+ system_id, "sda", model="QEMU", serial="QEMU0001", size=size, tags=[tag]
+ )
+ BlockDevices._handler.create.assert_called_once_with(
+ system_id=system_id,
+ name="sda",
+ model="QEMU",
+ serial="QEMU0001",
+ size=size,
+ block_size=512,
+ )
+ BlockDevice._handler.add_tag.assert_called_once_with(
+ system_id=system_id, id=block_id, tag=tag
+ )
+
+
+class TestBlockDevice(TestCase):
+ def test__read_bad_node_type(self):
+ BlockDevice = make_origin().BlockDevice
+ error = self.assertRaises(
+ TypeError, BlockDevice.read, random.randint(0, 100), random.randint(0, 100)
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ BlockDevice._handler.read.return_value = block
+ BlockDevice.read(system_id, block["id"])
+ BlockDevice._handler.read.assert_called_once_with(
+ system_id=system_id, id=block["id"]
+ )
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ BlockDevice = origin.BlockDevice
+ Node = origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ block = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ BlockDevice._handler.read.return_value = block
+ BlockDevice.read(node, block["id"])
+ BlockDevice._handler.read.assert_called_once_with(
+ system_id=system_id, id=block["id"]
+ )
+
+ def test__save_add_tag(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ "tags": [],
+ }
+ )
+ tag = make_string_without_spaces()
+ block.tags.append(tag)
+ BlockDevice._handler.add_tag.return_value = None
+ block.save()
+ BlockDevice._handler.add_tag.assert_called_once_with(
+ system_id=system_id, id=block.id, tag=tag
+ )
+
+ def test__save_remove_tag(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ tag = make_string_without_spaces()
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ "tags": [tag],
+ }
+ )
+ block.tags.remove(tag)
+ BlockDevice._handler.remove_tag.return_value = None
+ block.save()
+ BlockDevice._handler.remove_tag.assert_called_once_with(
+ system_id=system_id, id=block.id, tag=tag
+ )
+
+ def test__delete(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ )
+ BlockDevice._handler.delete.return_value = None
+ block.delete()
+ BlockDevice._handler.delete.assert_called_once_with(
+ system_id=system_id, id=block.id
+ )
+
+ def test__set_as_boot_disk(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ )
+ BlockDevice._handler.set_boot_disk.return_value = None
+ block.set_as_boot_disk()
+ BlockDevice._handler.set_boot_disk.assert_called_once_with(
+ system_id=system_id, id=block.id
+ )
+
+ def test__format(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block_id = random.randint(0, 100)
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ }
+ )
+ BlockDevice._handler.format.return_value = {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {"fstype": "ext4"},
+ }
+ block.format("ext4")
+ BlockDevice._handler.format.assert_called_once_with(
+ system_id=system_id, id=block_id, fstype="ext4", uuid=None
+ )
+
+ def test__unformat(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block_id = random.randint(0, 100)
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {"fstype": "ext4"},
+ }
+ )
+ BlockDevice._handler.unformat.return_value = {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": None,
+ }
+ block.unformat()
+ BlockDevice._handler.unformat.assert_called_once_with(
+ system_id=system_id, id=block_id
+ )
+
+ def test__mount(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block_id = random.randint(0, 100)
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {"fstype": "ext4"},
+ }
+ )
+ BlockDevice._handler.mount.return_value = {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {
+ "fstype": "ext4",
+ "mount_point": "/",
+ "mount_options": "noatime",
+ },
+ }
+ block.mount("/", mount_options="noatime")
+ BlockDevice._handler.mount.assert_called_once_with(
+ system_id=system_id, id=block_id, mount_point="/", mount_options="noatime"
+ )
+
+ def test__unmount(self):
+ BlockDevice = make_origin().BlockDevice
+ system_id = make_string_without_spaces()
+ block_id = random.randint(0, 100)
+ block = BlockDevice(
+ {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {
+ "fstype": "ext4",
+ "mount_point": "/",
+ "mount_options": "noatime",
+ },
+ }
+ )
+ BlockDevice._handler.unmount.return_value = {
+ "system_id": system_id,
+ "id": block_id,
+ "type": BlockDeviceType.PHYSICAL.value,
+ "filesystem": {"fstype": "ext4", "mount_point": "", "mount_options": ""},
+ }
+ block.unmount()
+ BlockDevice._handler.unmount.assert_called_once_with(
+ system_id=system_id, id=block_id
+ )
diff --git a/maas/client/viscera/tests/test_boot_resources.py b/maas/client/viscera/tests/test_boot_resources.py
index 58265821..796f6862 100644
--- a/maas/client/viscera/tests/test_boot_resources.py
+++ b/maas/client/viscera/tests/test_boot_resources.py
@@ -1,35 +1,25 @@
"""Test for `maas.client.viscera.boot_resources`."""
-__all__ = []
-
import hashlib
from http import HTTPStatus
import io
import random
-from unittest.mock import (
- call,
- MagicMock,
- sentinel,
-)
+from unittest.mock import call, MagicMock, sentinel
import aiohttp
-from testtools.matchers import (
- Equals,
- MatchesDict,
- MatchesStructure,
-)
+from aiohttp.test_utils import make_mocked_coro
+from testtools.matchers import Equals, MatchesDict, MatchesStructure
from .. import boot_resources
from ...testing import (
+ AsyncCallableMock,
+ AsyncContextMock,
make_name_without_spaces,
make_string,
pick_bool,
TestCase,
)
-from ..testing import (
- AsyncMock,
- bind,
-)
+from ..testing import bind
def make_origin():
@@ -39,33 +29,36 @@ def make_origin():
class TestBootResource(TestCase):
-
def test__string_representation_includes_type_name_architecture(self):
- source = boot_resources.BootResource({
- "id": random.randint(0, 100),
- "type": "Synced",
- "name": "ubuntu/xenial",
- "architecture": "amd64/ga-16.04",
- "subarches": "generic,ga-16.04",
- })
- self.assertThat(repr(source), Equals(
- "" % (
- source._data)))
+ source = boot_resources.BootResource(
+ {
+ "id": random.randint(0, 100),
+ "type": "Synced",
+ "name": "ubuntu/xenial",
+ "architecture": "amd64/ga-16.04",
+ "subarches": "generic,ga-16.04",
+ }
+ )
+ self.assertThat(
+ repr(source),
+ Equals(
+ "" % (source._data)
+ ),
+ )
def test__read(self):
resource_id = random.randint(0, 100)
rtype = random.choice(["Synced", "Uploaded", "Generated"])
name = "%s/%s" % (
make_name_without_spaces("os"),
- make_name_without_spaces("release"))
+ make_name_without_spaces("release"),
+ )
architecture = "%s/%s" % (
make_name_without_spaces("arch"),
- make_name_without_spaces("subarch"))
- subarches = ",".join(
- make_name_without_spaces("subarch")
- for _ in range(3)
+ make_name_without_spaces("subarch"),
)
+ subarches = ",".join(make_name_without_spaces("subarch") for _ in range(3))
sets = {}
for _ in range(3):
version = make_name_without_spaces("version")
@@ -89,65 +82,74 @@ def test__read(self):
BootResource = make_origin().BootResource
BootResource._handler.read.return_value = {
- "id": resource_id, "type": rtype, "name": name,
- "architecture": architecture, "subarches": subarches,
- "sets": sets}
+ "id": resource_id,
+ "type": rtype,
+ "name": name,
+ "architecture": architecture,
+ "subarches": subarches,
+ "sets": sets,
+ }
resource = BootResource.read(resource_id)
BootResource._handler.read.assert_called_once_with(id=resource_id)
- self.assertThat(resource, MatchesStructure(
- id=Equals(resource_id),
- type=Equals(rtype),
- name=Equals(name),
- architecture=Equals(architecture),
- subarches=Equals(subarches),
- sets=MatchesDict({
- version: MatchesStructure(
- version=Equals(version),
- size=Equals(rset["size"]),
- label=Equals(rset["label"]),
- complete=Equals(rset["complete"]),
- files=MatchesDict({
- filename: MatchesStructure(
- filename=Equals(filename),
- filetype=Equals(rfile["filetype"]),
- size=Equals(rfile["size"]),
- sha256=Equals(rfile["sha256"]),
- complete=Equals(rfile["complete"]),
+ self.assertThat(
+ resource,
+ MatchesStructure(
+ id=Equals(resource_id),
+ type=Equals(rtype),
+ name=Equals(name),
+ architecture=Equals(architecture),
+ subarches=Equals(subarches),
+ sets=MatchesDict(
+ {
+ version: MatchesStructure(
+ version=Equals(version),
+ size=Equals(rset["size"]),
+ label=Equals(rset["label"]),
+ complete=Equals(rset["complete"]),
+ files=MatchesDict(
+ {
+ filename: MatchesStructure(
+ filename=Equals(filename),
+ filetype=Equals(rfile["filetype"]),
+ size=Equals(rfile["size"]),
+ sha256=Equals(rfile["sha256"]),
+ complete=Equals(rfile["complete"]),
+ )
+ for filename, rfile in rset["files"].items()
+ }
+ ),
)
- for filename, rfile in rset["files"].items()
- }),
- )
- for version, rset in sets.items()
- })))
+ for version, rset in sets.items()
+ }
+ ),
+ ),
+ )
def test__delete(self):
resource_id = random.randint(0, 100)
BootResource = make_origin().BootResource
- resource = BootResource({
- "id": resource_id,
- "type": "Synced",
- "name": "ubuntu/xenial",
- "architecture": "amd64/ga-16.04",
- "subarches": "generic,ga-16.04",
- })
+ resource = BootResource(
+ {
+ "id": resource_id,
+ "type": "Synced",
+ "name": "ubuntu/xenial",
+ "architecture": "amd64/ga-16.04",
+ "subarches": "generic,ga-16.04",
+ }
+ )
resource.delete()
BootResource._handler.delete.assert_called_once_with(id=resource_id)
class TestBootResources(TestCase):
-
def test__read(self):
BootResources = make_origin().BootResources
BootResources._handler.read.return_value = [
- {
- "id": random.randint(0, 9),
- },
- {
- "id": random.randint(10, 19),
- },
+ {"id": random.randint(0, 9)},
+ {"id": random.randint(10, 19)},
]
resources = BootResources.read()
@@ -172,20 +174,19 @@ def test__create_raises_ValueError_when_name_missing_slash(self):
BootResources = make_origin().BootResources
buf = io.BytesIO(b"")
- error = self.assertRaises(
- ValueError, BootResources.create, "", "", buf)
- self.assertEquals(
- "name must be in format os/release; missing '/'", str(error))
+ error = self.assertRaises(ValueError, BootResources.create, "", "", buf)
+ self.assertEquals("name must be in format os/release; missing '/'", str(error))
def test__create_raises_ValueError_when_architecture_missing_slash(self):
BootResources = make_origin().BootResources
buf = io.BytesIO(b"")
error = self.assertRaises(
- ValueError, BootResources.create, "os/release", "", buf)
+ ValueError, BootResources.create, "os/release", "", buf
+ )
self.assertEquals(
- "architecture must be in format arch/subarch; missing '/'",
- str(error))
+ "architecture must be in format arch/subarch; missing '/'", str(error)
+ )
def test__create_raises_ValueError_when_content_cannot_be_read(self):
BootResources = make_origin().BootResources
@@ -193,10 +194,9 @@ def test__create_raises_ValueError_when_content_cannot_be_read(self):
buf = io.BytesIO(b"")
self.patch(buf, "readable").return_value = False
error = self.assertRaises(
- ValueError, BootResources.create,
- "os/release", "arch/subarch", buf)
- self.assertEquals(
- "content must be readable", str(error))
+ ValueError, BootResources.create, "os/release", "arch/subarch", buf
+ )
+ self.assertEquals("content must be readable", str(error))
def test__create_raises_ValueError_when_content_cannot_seek(self):
BootResources = make_origin().BootResources
@@ -204,43 +204,55 @@ def test__create_raises_ValueError_when_content_cannot_seek(self):
buf = io.BytesIO(b"")
self.patch(buf, "seekable").return_value = False
error = self.assertRaises(
- ValueError, BootResources.create,
- "os/release", "arch/subarch", buf)
- self.assertEquals(
- "content must be seekable", str(error))
+ ValueError, BootResources.create, "os/release", "arch/subarch", buf
+ )
+ self.assertEquals("content must be seekable", str(error))
def test__create_raises_ValueError_when_chunk_size_is_zero(self):
BootResources = make_origin().BootResources
buf = io.BytesIO(b"")
error = self.assertRaises(
- ValueError, BootResources.create,
- "os/release", "arch/subarch", buf, chunk_size=0)
- self.assertEquals(
- "chunk_size must be greater than 0, not 0", str(error))
+ ValueError,
+ BootResources.create,
+ "os/release",
+ "arch/subarch",
+ buf,
+ chunk_size=0,
+ )
+ self.assertEquals("chunk_size must be greater than 0, not 0", str(error))
def test__create_raises_ValueError_when_chunk_size_is_less_than_zero(self):
BootResources = make_origin().BootResources
buf = io.BytesIO(b"")
error = self.assertRaises(
- ValueError, BootResources.create,
- "os/release", "arch/subarch", buf, chunk_size=-1)
- self.assertEquals(
- "chunk_size must be greater than 0, not -1", str(error))
+ ValueError,
+ BootResources.create,
+ "os/release",
+ "arch/subarch",
+ buf,
+ chunk_size=-1,
+ )
+ self.assertEquals("chunk_size must be greater than 0, not -1", str(error))
def test__create_calls_create_on_handler_does_nothing_if_complete(self):
resource_id = random.randint(0, 100)
name = "%s/%s" % (
make_name_without_spaces("os"),
- make_name_without_spaces("release"))
+ make_name_without_spaces("release"),
+ )
architecture = "%s/%s" % (
make_name_without_spaces("arch"),
- make_name_without_spaces("subarch"))
+ make_name_without_spaces("subarch"),
+ )
title = make_name_without_spaces("title")
- filetype = random.choice([
- boot_resources.BootResourceFileType.TGZ,
- boot_resources.BootResourceFileType.DDTGZ])
+ filetype = random.choice(
+ [
+ boot_resources.BootResourceFileType.TGZ,
+ boot_resources.BootResourceFileType.DDTGZ,
+ ]
+ )
data = make_string().encode("ascii")
sha256 = hashlib.sha256()
@@ -269,31 +281,40 @@ def test__create_calls_create_on_handler_does_nothing_if_complete(self):
"sha256": sha256,
"complete": True,
}
- }
+ },
}
- }
+ },
}
mock_upload_chunks = self.patch(BootResources, "_upload_chunks")
resource = BootResources.create(
- name, architecture, buf, title=title, filetype=filetype)
- self.assertThat(resource, MatchesStructure.byEquality(
- id=resource_id, type="Uploaded",
- name=name, architecture=architecture))
+ name, architecture, buf, title=title, filetype=filetype
+ )
+ self.assertThat(
+ resource,
+ MatchesStructure.byEquality(
+ id=resource_id, type="Uploaded", name=name, architecture=architecture
+ ),
+ )
self.assertFalse(mock_upload_chunks.called)
def test__create_uploads_in_chunks_and_reloads_resource(self):
resource_id = random.randint(0, 100)
name = "%s/%s" % (
make_name_without_spaces("os"),
- make_name_without_spaces("release"))
+ make_name_without_spaces("release"),
+ )
architecture = "%s/%s" % (
make_name_without_spaces("arch"),
- make_name_without_spaces("subarch"))
+ make_name_without_spaces("subarch"),
+ )
title = make_name_without_spaces("title")
- filetype = random.choice([
- boot_resources.BootResourceFileType.TGZ,
- boot_resources.BootResourceFileType.DDTGZ])
+ filetype = random.choice(
+ [
+ boot_resources.BootResourceFileType.TGZ,
+ boot_resources.BootResourceFileType.DDTGZ,
+ ]
+ )
upload_uri = "/MAAS/api/2.0/boot-resources/%d/upload/1" % resource_id
# Make chunks and upload in pieces of 4, where the last piece is
@@ -312,7 +333,8 @@ def test__create_uploads_in_chunks_and_reloads_resource(self):
BootResources = origin.BootResources
BootResource = origin.BootResource
BootResources._handler.uri = (
- "http://localhost:5240/MAAS/api/2.0/boot-resources/")
+ "http://localhost:5240/MAAS/api/2.0/boot-resources/"
+ )
BootResources._handler.create.return_value = {
"id": resource_id,
"type": "Uploaded",
@@ -333,9 +355,9 @@ def test__create_uploads_in_chunks_and_reloads_resource(self):
"complete": False,
"upload_uri": upload_uri,
}
- }
+ },
}
- }
+ },
}
BootResource._handler.read.return_value = {
"id": resource_id,
@@ -356,18 +378,19 @@ def test__create_uploads_in_chunks_and_reloads_resource(self):
"sha256": sha256,
"complete": True,
}
- }
+ },
}
- }
+ },
}
# Mock signing. Test checks that its actually called.
mock_sign = self.patch(boot_resources.utils, "sign")
# Mock ClientSession.put as the create does PUT directly to the API.
- put = AsyncMock()
- response = put.return_value = AsyncMock(spec=aiohttp.ClientResponse)
+ response = AsyncContextMock(spec=aiohttp.ClientResponse)
response.status = HTTPStatus.OK.value
+
+ put = AsyncCallableMock(return_value=response)
self.patch(boot_resources.aiohttp.ClientSession, "put", put)
# Progress handler called on each chunk.
@@ -375,14 +398,22 @@ def test__create_uploads_in_chunks_and_reloads_resource(self):
# Create and upload the resource.
resource = BootResources.create(
- name, architecture, buf,
- title=title, filetype=filetype, chunk_size=chunk_size,
- progress_callback=progress_handler)
+ name,
+ architecture,
+ buf,
+ title=title,
+ filetype=filetype,
+ chunk_size=chunk_size,
+ progress_callback=progress_handler,
+ )
# Check that returned resource is correct and updated.
- self.assertThat(resource, MatchesStructure.byEquality(
- id=resource_id, type="Uploaded",
- name=name, architecture=architecture))
+ self.assertThat(
+ resource,
+ MatchesStructure.byEquality(
+ id=resource_id, type="Uploaded", name=name, architecture=architecture
+ ),
+ )
self.assertTrue(resource.sets["20161026"].complete)
# Check that the request was signed.
@@ -393,17 +424,19 @@ def test__create_uploads_in_chunks_and_reloads_resource(self):
call(
"http://localhost:5240/MAAS/api/2.0/"
"boot-resources/%d/upload/1" % resource_id,
- data=data[0 + i:chunk_size + i], headers={
- 'Content-Type': 'application/octet-stream',
- 'Content-Length': str(len(data[0 + i:chunk_size + i])),
- })
+ data=data[0 + i : chunk_size + i],
+ headers={
+ "Content-Type": "application/octet-stream",
+ "Content-Length": str(len(data[0 + i : chunk_size + i])),
+ },
+ )
for i in range(0, len(data), chunk_size)
]
self.assertEquals(calls, put.call_args_list)
# Check that progress handler was called on each chunk.
calls = [
- call(len(data[:chunk_size + i]) / len(data))
+ call(len(data[: chunk_size + i]) / len(data))
for i in range(0, len(data), chunk_size)
]
self.assertEquals(calls, progress_handler.call_args_list)
@@ -412,14 +445,19 @@ def test__create_raises_CallError_on_chunk_upload_failure(self):
resource_id = random.randint(0, 100)
name = "%s/%s" % (
make_name_without_spaces("os"),
- make_name_without_spaces("release"))
+ make_name_without_spaces("release"),
+ )
architecture = "%s/%s" % (
make_name_without_spaces("arch"),
- make_name_without_spaces("subarch"))
+ make_name_without_spaces("subarch"),
+ )
title = make_name_without_spaces("title")
- filetype = random.choice([
- boot_resources.BootResourceFileType.TGZ,
- boot_resources.BootResourceFileType.DDTGZ])
+ filetype = random.choice(
+ [
+ boot_resources.BootResourceFileType.TGZ,
+ boot_resources.BootResourceFileType.DDTGZ,
+ ]
+ )
upload_uri = "/MAAS/api/2.0/boot-resources/%d/upload/1" % resource_id
# Make chunks and upload in pieces of 4, where the last piece is
@@ -437,7 +475,8 @@ def test__create_raises_CallError_on_chunk_upload_failure(self):
origin = make_origin()
BootResources = origin.BootResources
BootResources._handler.uri = (
- "http://localhost:5240/MAAS/api/2.0/boot-resources/")
+ "http://localhost:5240/MAAS/api/2.0/boot-resources/"
+ )
BootResources._handler.path = "/MAAS/api/2.0/boot-resources/"
BootResources._handler.create.return_value = {
"id": resource_id,
@@ -459,25 +498,32 @@ def test__create_raises_CallError_on_chunk_upload_failure(self):
"complete": False,
"upload_uri": upload_uri,
}
- }
+ },
}
- }
+ },
}
# Mock signing. Test checks that its actually called.
mock_sign = self.patch(boot_resources.utils, "sign")
# Mock ClientSession.put as the create does PUT directly to the API.
- put = AsyncMock()
- response = put.return_value = AsyncMock(spec=aiohttp.ClientResponse)
+ response = AsyncContextMock(spec=aiohttp.ClientResponse)
response.status = HTTPStatus.INTERNAL_SERVER_ERROR.value
- response.read.return_value = b"Error"
+ response.read = make_mocked_coro(b"Error")
+
+ put = AsyncCallableMock(return_value=response)
self.patch(boot_resources.aiohttp.ClientSession, "put", put)
self.assertRaises(
- boot_resources.CallError, BootResources.create,
- name, architecture, buf,
- title=title, filetype=filetype, chunk_size=chunk_size)
+ boot_resources.CallError,
+ BootResources.create,
+ name,
+ architecture,
+ buf,
+ title=title,
+ filetype=filetype,
+ chunk_size=chunk_size,
+ )
# Check that the request was signed.
self.assertTrue(mock_sign.called)
diff --git a/maas/client/viscera/tests/test_boot_source_selections.py b/maas/client/viscera/tests/test_boot_source_selections.py
index b030a420..54370f75 100644
--- a/maas/client/viscera/tests/test_boot_source_selections.py
+++ b/maas/client/viscera/tests/test_boot_source_selections.py
@@ -1,22 +1,11 @@
"""Test for `maas.client.viscera.boot_source_selections`."""
-__all__ = []
-
import random
-from testtools.matchers import (
- Equals,
- MatchesStructure,
-)
-
-from .. import (
- boot_source_selections,
- boot_sources,
-)
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
+from testtools.matchers import Equals, MatchesStructure
+
+from .. import boot_source_selections, boot_sources
+from ...testing import make_name_without_spaces, TestCase
from ..testing import bind
@@ -26,41 +15,42 @@ def make_origin():
# bound.
return bind(
boot_source_selections.BootSourceSelections,
- boot_source_selections.BootSourceSelection)
+ boot_source_selections.BootSourceSelection,
+ boot_sources.BootSource,
+ )
def make_boot_source():
- return boot_sources.BootSource({
- "id": random.randint(0, 100),
- "url": "http://images.maas.io/ephemeral-v3/daily/",
- "keyring_filename": (
- "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"),
- "keyring_data": "",
- })
+ return boot_sources.BootSource(
+ {
+ "id": random.randint(0, 100),
+ "url": "http://images.maas.io/ephemeral-v3/daily/",
+ "keyring_filename": ("/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"),
+ "keyring_data": "",
+ }
+ )
class TestBootSourceSelection(TestCase):
-
def test__string_representation_includes_defined_keys(self):
- selection = boot_source_selections.BootSourceSelection({
- "os": make_name_without_spaces("os"),
- "release": make_name_without_spaces("release"),
- "arches": [make_name_without_spaces("arch")],
- "subarches": [make_name_without_spaces("subarches")],
- "labels": [make_name_without_spaces("labels")],
- }, {
- "boot_source_id": random.randint(0, 100),
- })
- self.assertThat(repr(selection), Equals(
- "" % (
- selection._data)))
-
- def test__read_raises_TypeError_when_no_BootSource(self):
- BootSourceSelection = make_origin().BootSourceSelection
- self.assertRaises(
- TypeError, BootSourceSelection.read,
- random.randint(0, 100), random.randint(0, 100))
+ selection = boot_source_selections.BootSourceSelection(
+ {
+ "os": make_name_without_spaces("os"),
+ "release": make_name_without_spaces("release"),
+ "arches": [make_name_without_spaces("arch")],
+ "subarches": [make_name_without_spaces("subarches")],
+ "labels": [make_name_without_spaces("labels")],
+ },
+ {"boot_source_id": random.randint(0, 100)},
+ )
+ self.assertThat(
+ repr(selection),
+ Equals(
+ ""
+ % (selection._data)
+ ),
+ )
def test__read(self):
source = make_boot_source()
@@ -71,17 +61,35 @@ def test__read(self):
subarches = [make_name_without_spaces("subarches")]
labels = [make_name_without_spaces("labels")]
- BootSourceSelection = make_origin().BootSourceSelection
+ origin = make_origin()
+ BootSourceSelection = origin.BootSourceSelection
BootSourceSelection._handler.read.return_value = {
- "id": selection_id, "os": os, "release": release,
- "arches": arches, "subarches": subarches, "labels": labels}
+ "id": selection_id,
+ "os": os,
+ "release": release,
+ "arches": arches,
+ "subarches": subarches,
+ "labels": labels,
+ }
selection = BootSourceSelection.read(source, selection_id)
BootSourceSelection._handler.read.assert_called_once_with(
- boot_source_id=source.id, id=selection_id)
- self.assertThat(selection, MatchesStructure.byEquality(
- id=selection_id, boot_source_id=source.id, os=os, release=release,
- arches=arches, subarches=subarches, labels=labels))
+ boot_source_id=source.id, id=selection_id
+ )
+ self.assertThat(
+ selection,
+ MatchesStructure.byEquality(
+ id=selection_id,
+ boot_source=origin.BootSource(
+ source.id, {"bootsourceselection": selection}
+ ),
+ os=os,
+ release=release,
+ arches=arches,
+ subarches=subarches,
+ labels=labels,
+ ),
+ )
def test__delete(self):
source = make_boot_source()
@@ -93,39 +101,48 @@ def test__delete(self):
labels = [make_name_without_spaces("labels")]
BootSourceSelection = make_origin().BootSourceSelection
- selection = BootSourceSelection({
- "id": selection_id, "os": os, "release": release,
- "arches": arches, "subarches": subarches, "labels": labels}, {
- "boot_source_id": source.id})
+ selection = BootSourceSelection(
+ {
+ "id": selection_id,
+ "os": os,
+ "release": release,
+ "arches": arches,
+ "subarches": subarches,
+ "labels": labels,
+ },
+ {"boot_source_id": source.id},
+ )
selection.delete()
BootSourceSelection._handler.delete.assert_called_once_with(
- boot_source_id=source.id, id=selection_id)
+ boot_source_id=source.id, id=selection_id
+ )
class TestBootSources(TestCase):
-
def test__read_raises_TypeError_when_no_BootSource(self):
BootSourceSelections = make_origin().BootSourceSelections
- self.assertRaises(
- TypeError, BootSourceSelections.read, random.randint(0, 100))
+ self.assertRaises(TypeError, BootSourceSelections.read, random.randint(0, 100))
def test__read(self):
source = make_boot_source()
- BootSourceSelections = make_origin().BootSourceSelections
+ origin = make_origin()
+ BootSourceSelections = origin.BootSourceSelections
BootSourceSelections._handler.read.return_value = [
- {
- "id": random.randint(0, 9),
- },
- {
- "id": random.randint(10, 19),
- },
+ {"id": random.randint(0, 9)},
+ {"id": random.randint(10, 19)},
]
selections = BootSourceSelections.read(source)
self.assertEquals(2, len(selections))
- self.assertEquals(source.id, selections[0].boot_source_id)
- self.assertEquals(source.id, selections[1].boot_source_id)
+ self.assertEquals(
+ origin.BootSource(source.id, {"bootsourceselection": selections[0]}),
+ selections[0].boot_source,
+ )
+ self.assertEquals(
+ origin.BootSource(source.id, {"bootsourceselection": selections[1]}),
+ selections[1].boot_source,
+ )
def test__create_raises_TypeError_when_no_BootSource(self):
os = make_name_without_spaces("os")
@@ -134,8 +151,8 @@ def test__create_raises_TypeError_when_no_BootSource(self):
BootSourceSelections = make_origin().BootSourceSelections
self.assertRaises(
- TypeError, BootSourceSelections.create,
- random.randint(0, 100), os, release)
+ TypeError, BootSourceSelections.create, random.randint(0, 100), os, release
+ )
def test__create__without_kwargs(self):
source = make_boot_source()
@@ -143,18 +160,40 @@ def test__create__without_kwargs(self):
os = make_name_without_spaces("os")
release = make_name_without_spaces("release")
- BootSourceSelections = make_origin().BootSourceSelections
+ origin = make_origin()
+ BootSourceSelections = origin.BootSourceSelections
BootSourceSelections._handler.create.return_value = {
- "id": selection_id, "os": os, "release": release,
- "arches": ["*"], "subarches": ["*"], "labels": ["*"]}
+ "id": selection_id,
+ "os": os,
+ "release": release,
+ "arches": ["*"],
+ "subarches": ["*"],
+ "labels": ["*"],
+ }
selection = BootSourceSelections.create(source, os, release)
BootSourceSelections._handler.create.assert_called_once_with(
- boot_source_id=source.id, os=os, release=release,
- arches=["*"], subarches=["*"], labels=["*"])
- self.assertThat(selection, MatchesStructure.byEquality(
- id=selection_id, boot_source_id=source.id, os=os, release=release,
- arches=["*"], subarches=["*"], labels=["*"]))
+ boot_source_id=source.id,
+ os=os,
+ release=release,
+ arches=["*"],
+ subarches=["*"],
+ labels=["*"],
+ )
+ self.assertThat(
+ selection,
+ MatchesStructure.byEquality(
+ id=selection_id,
+ boot_source=origin.BootSource(
+ source.id, {"bootsourceselection": selection}
+ ),
+ os=os,
+ release=release,
+ arches=["*"],
+ subarches=["*"],
+ labels=["*"],
+ ),
+ )
def test__create__with_kwargs(self):
source = make_boot_source()
@@ -165,17 +204,39 @@ def test__create__with_kwargs(self):
subarches = [make_name_without_spaces("subarches")]
labels = [make_name_without_spaces("labels")]
- BootSourceSelections = make_origin().BootSourceSelections
+ origin = make_origin()
+ BootSourceSelections = origin.BootSourceSelections
BootSourceSelections._handler.create.return_value = {
- "id": selection_id, "os": os, "release": release,
- "arches": arches, "subarches": subarches, "labels": labels}
+ "id": selection_id,
+ "os": os,
+ "release": release,
+ "arches": arches,
+ "subarches": subarches,
+ "labels": labels,
+ }
selection = BootSourceSelections.create(
- source, os, release,
- arches=arches, subarches=subarches, labels=labels)
+ source, os, release, arches=arches, subarches=subarches, labels=labels
+ )
BootSourceSelections._handler.create.assert_called_once_with(
- boot_source_id=source.id, os=os, release=release,
- arches=arches, subarches=subarches, labels=labels)
- self.assertThat(selection, MatchesStructure.byEquality(
- id=selection_id, boot_source_id=source.id, os=os, release=release,
- arches=arches, subarches=subarches, labels=labels))
+ boot_source_id=source.id,
+ os=os,
+ release=release,
+ arches=arches,
+ subarches=subarches,
+ labels=labels,
+ )
+ self.assertThat(
+ selection,
+ MatchesStructure.byEquality(
+ id=selection_id,
+ boot_source=origin.BootSource(
+ source.id, {"bootsourceselection": selection}
+ ),
+ os=os,
+ release=release,
+ arches=arches,
+ subarches=subarches,
+ labels=labels,
+ ),
+ )
diff --git a/maas/client/viscera/tests/test_boot_sources.py b/maas/client/viscera/tests/test_boot_sources.py
index b2f7b799..943afc19 100644
--- a/maas/client/viscera/tests/test_boot_sources.py
+++ b/maas/client/viscera/tests/test_boot_sources.py
@@ -1,19 +1,11 @@
"""Test for `maas.client.viscera.boot_sources`."""
-__all__ = []
-
import random
-from testtools.matchers import (
- Equals,
- MatchesStructure,
-)
+from testtools.matchers import Equals, MatchesStructure
from .. import boot_sources
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
+from ...testing import make_name_without_spaces, TestCase
from ..testing import bind
@@ -24,95 +16,105 @@ def make_origin():
class TestBootSource(TestCase):
-
def test__string_representation_includes_url_keyring_info_only(self):
- source = boot_sources.BootSource({
- "url": "http://images.maas.io/ephemeral-v3/daily/",
- "keyring_filename": (
- "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"),
- "keyring_data": "",
- })
- self.assertThat(repr(source), Equals(
- "" % (
- source._data)))
+ source = boot_sources.BootSource(
+ {
+ "url": "http://images.maas.io/ephemeral-v3/daily/",
+ "keyring_filename": (
+ "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"
+ ),
+ "keyring_data": "",
+ }
+ )
+ self.assertThat(
+ repr(source),
+ Equals(
+ "" % (source._data)
+ ),
+ )
def test__read(self):
source_id = random.randint(0, 100)
url = "http://images.maas.io/ephemeral-v3/daily/"
- keyring_filename = (
- "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg")
+ keyring_filename = "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"
BootSource = make_origin().BootSource
BootSource._handler.read.return_value = {
- "id": source_id, "url": url,
- "keyring_filename": keyring_filename, "keyring_data": ""}
+ "id": source_id,
+ "url": url,
+ "keyring_filename": keyring_filename,
+ "keyring_data": "",
+ }
source = BootSource.read(source_id)
BootSource._handler.read.assert_called_once_with(id=source_id)
- self.assertThat(source, MatchesStructure.byEquality(
- id=source_id, url=url, keyring_filename=keyring_filename,
- keyring_data=""))
+ self.assertThat(
+ source,
+ MatchesStructure.byEquality(
+ id=source_id,
+ url=url,
+ keyring_filename=keyring_filename,
+ keyring_data="",
+ ),
+ )
def test__delete(self):
source_id = random.randint(0, 100)
BootSource = make_origin().BootSource
- source = BootSource({
- "id": source_id,
- "url": "http://images.maas.io/ephemeral-v3/daily/",
- "keyring_filename": (
- "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"),
- "keyring_data": "",
- })
+ source = BootSource(
+ {
+ "id": source_id,
+ "url": "http://images.maas.io/ephemeral-v3/daily/",
+ "keyring_filename": (
+ "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"
+ ),
+ "keyring_data": "",
+ }
+ )
source.delete()
BootSource._handler.delete.assert_called_once_with(id=source_id)
class TestBootSources(TestCase):
-
def test__read(self):
BootSources = make_origin().BootSources
BootSources._handler.read.return_value = [
- {
- "id": random.randint(0, 9),
- },
- {
- "id": random.randint(10, 19),
- },
+ {"id": random.randint(0, 9)},
+ {"id": random.randint(10, 19)},
]
sources = BootSources.read()
self.assertEquals(2, len(sources))
- def test__create_raises_ValueError_when_signed(self):
- url = "http://images.maas.io/ephemeral-v3/daily/"
-
- BootSources = make_origin().BootSources
-
- error = self.assertRaises(ValueError, BootSources.create, url)
- self.assertEquals(
- "Either keyring_filename and keyring_data must be set when "
- "providing a signed source.", str(error))
-
def test__create_calls_create_with_keyring_filename(self):
source_id = random.randint(0, 100)
url = "http://images.maas.io/ephemeral-v3/daily/"
- keyring_filename = (
- "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg")
+ keyring_filename = "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg"
BootSources = make_origin().BootSources
BootSources._handler.create.return_value = {
- "id": source_id, "url": url,
- "keyring_filename": keyring_filename, "keyring_data": ""}
+ "id": source_id,
+ "url": url,
+ "keyring_filename": keyring_filename,
+ "keyring_data": "",
+ }
source = BootSources.create(url, keyring_filename=keyring_filename)
BootSources._handler.create.assert_called_once_with(
- url=url, keyring_filename=keyring_filename, keyring_data="")
- self.assertThat(source, MatchesStructure.byEquality(
- id=source_id, url=url,
- keyring_filename=keyring_filename, keyring_data=""))
+ url=url, keyring_filename=keyring_filename, keyring_data=""
+ )
+ self.assertThat(
+ source,
+ MatchesStructure.byEquality(
+ id=source_id,
+ url=url,
+ keyring_filename=keyring_filename,
+ keyring_data="",
+ ),
+ )
def test__create_calls_create_with_keyring_data(self):
source_id = random.randint(0, 100)
@@ -121,15 +123,22 @@ def test__create_calls_create_with_keyring_data(self):
BootSources = make_origin().BootSources
BootSources._handler.create.return_value = {
- "id": source_id, "url": url,
- "keyring_filename": "", "keyring_data": keyring_data}
+ "id": source_id,
+ "url": url,
+ "keyring_filename": "",
+ "keyring_data": keyring_data,
+ }
source = BootSources.create(url, keyring_data=keyring_data)
BootSources._handler.create.assert_called_once_with(
- url=url, keyring_filename="", keyring_data=keyring_data)
- self.assertThat(source, MatchesStructure.byEquality(
- id=source_id, url=url,
- keyring_filename="", keyring_data=keyring_data))
+ url=url, keyring_filename="", keyring_data=keyring_data
+ )
+ self.assertThat(
+ source,
+ MatchesStructure.byEquality(
+ id=source_id, url=url, keyring_filename="", keyring_data=keyring_data
+ ),
+ )
def test__create_calls_create_with_unsigned_url(self):
source_id = random.randint(0, 100)
@@ -137,12 +146,19 @@ def test__create_calls_create_with_unsigned_url(self):
BootSources = make_origin().BootSources
BootSources._handler.create.return_value = {
- "id": source_id, "url": url,
- "keyring_filename": "", "keyring_data": ""}
+ "id": source_id,
+ "url": url,
+ "keyring_filename": "",
+ "keyring_data": "",
+ }
source = BootSources.create(url)
BootSources._handler.create.assert_called_once_with(
- url=url, keyring_filename="", keyring_data="")
- self.assertThat(source, MatchesStructure.byEquality(
- id=source_id, url=url,
- keyring_filename="", keyring_data=""))
+ url=url, keyring_filename="", keyring_data=""
+ )
+ self.assertThat(
+ source,
+ MatchesStructure.byEquality(
+ id=source_id, url=url, keyring_filename="", keyring_data=""
+ ),
+ )
diff --git a/maas/client/viscera/tests/test_controllers.py b/maas/client/viscera/tests/test_controllers.py
new file mode 100644
index 00000000..21ece48c
--- /dev/null
+++ b/maas/client/viscera/tests/test_controllers.py
@@ -0,0 +1,124 @@
+"""Test for `maas.client.viscera.controllers`."""
+
+from testtools.matchers import Equals
+
+from .. import controllers
+from ...testing import make_name_without_spaces, TestCase
+from ..testing import bind
+
+
+def make_origin():
+ # Create a new origin with RackController, RegionControllers,
+ # RackController, and RegionController.
+ return bind(
+ controllers.RackControllers,
+ controllers.RackController,
+ controllers.RegionControllers,
+ controllers.RegionController,
+ )
+
+
+class TestRackController(TestCase):
+ def test__string_representation_includes_only_system_id_and_hostname(self):
+ rack_controller = controllers.RackController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ self.assertThat(
+ repr(rack_controller),
+ Equals(
+ ""
+ % rack_controller._data
+ ),
+ )
+
+ def test__get_power_parameters(self):
+ rack_controller = make_origin().RackController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ rack_controller._handler.power_parameters.return_value = power_parameters
+ self.assertThat(
+ rack_controller.get_power_parameters(), Equals(power_parameters)
+ )
+ rack_controller._handler.power_parameters.assert_called_once_with(
+ system_id=rack_controller.system_id
+ )
+
+ def test__set_power(self):
+ orig_power_type = make_name_without_spaces("power_type")
+ new_power_type = make_name_without_spaces("power_type")
+ rack_controller = make_origin().RackController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "power_type": orig_power_type,
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ rack_controller._handler.update.return_value = {"power_type": new_power_type}
+ rack_controller.set_power(new_power_type, power_parameters)
+ rack_controller._handler.update.assert_called_once_with(
+ system_id=rack_controller.system_id,
+ power_type=new_power_type,
+ power_parameters=power_parameters,
+ )
+ self.assertThat(rack_controller.power_type, Equals(new_power_type))
+
+
+class TestRegionController(TestCase):
+ def test__string_representation_includes_only_system_id_and_hostname(self):
+ rack_controller = controllers.RegionController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ self.assertThat(
+ repr(rack_controller),
+ Equals(
+ ""
+ % rack_controller._data
+ ),
+ )
+
+ def test__get_power_parameters(self):
+ region_controller = make_origin().RegionController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ region_controller._handler.power_parameters.return_value = power_parameters
+ self.assertThat(
+ region_controller.get_power_parameters(), Equals(power_parameters)
+ )
+ region_controller._handler.power_parameters.assert_called_once_with(
+ system_id=region_controller.system_id
+ )
+
+ def test__set_power(self):
+ orig_power_type = make_name_without_spaces("power_type")
+ new_power_type = make_name_without_spaces("power_type")
+ region_controller = make_origin().RegionController(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "power_type": orig_power_type,
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ region_controller._handler.update.return_value = {"power_type": new_power_type}
+ region_controller.set_power(new_power_type, power_parameters)
+ region_controller._handler.update.assert_called_once_with(
+ system_id=region_controller.system_id,
+ power_type=new_power_type,
+ power_parameters=power_parameters,
+ )
+ self.assertThat(region_controller.power_type, Equals(new_power_type))
diff --git a/maas/client/viscera/tests/test_devices.py b/maas/client/viscera/tests/test_devices.py
index 32994f31..22077751 100644
--- a/maas/client/viscera/tests/test_devices.py
+++ b/maas/client/viscera/tests/test_devices.py
@@ -1,14 +1,9 @@
"""Test for `maas.client.viscera.devices`."""
-__all__ = []
-
-from testtools.matchers import Equals
+from testtools.matchers import Equals, IsInstance
from .. import devices
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
+from ...testing import make_name_without_spaces, TestCase
from ..testing import bind
@@ -19,15 +14,19 @@ def make_origin():
class TestDevice(TestCase):
-
def test__string_representation_includes_only_system_id_and_hostname(self):
- device = devices.Device({
- "system_id": make_name_without_spaces("system-id"),
- "hostname": make_name_without_spaces("hostname"),
- })
- self.assertThat(repr(device), Equals(
- ""
- % device._data))
+ device = devices.Device(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ self.assertThat(
+ repr(device),
+ Equals(
+ "" % device._data
+ ),
+ )
def test__read(self):
data = {
@@ -42,8 +41,69 @@ def test__read(self):
device_expected = origin.Device(data)
self.assertThat(device_observed, Equals(device_expected))
+ def test__get_power_parameters(self):
+ device = make_origin().Device(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ device._handler.power_parameters.return_value = power_parameters
+ self.assertThat(device.get_power_parameters(), Equals(power_parameters))
+ device._handler.power_parameters.assert_called_once_with(
+ system_id=device.system_id
+ )
+
+ def test__set_power(self):
+ orig_power_type = make_name_without_spaces("power_type")
+ new_power_type = make_name_without_spaces("power_type")
+ device = make_origin().Device(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "power_type": orig_power_type,
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ device._handler.update.return_value = {"power_type": new_power_type}
+ device.set_power(new_power_type, power_parameters)
+ device._handler.update.assert_called_once_with(
+ system_id=device.system_id,
+ power_type=new_power_type,
+ power_parameters=power_parameters,
+ )
+ self.assertThat(device.power_type, Equals(new_power_type))
+
class TestDevices(TestCase):
+ def test__create(self):
+ origin = make_origin()
+ Devices = origin.Devices
+ Devices._handler.create.return_value = {}
+ observed = Devices.create(
+ ["00:11:22:33:44:55", "00:11:22:33:44:AA"],
+ hostname="new-machine",
+ domain="maas",
+ zone="zone1",
+ )
+ self.assertThat(observed, IsInstance(devices.Device))
+ Devices._handler.create.assert_called_once_with(
+ mac_addresses=["00:11:22:33:44:55", "00:11:22:33:44:AA"],
+ hostname="new-machine",
+ domain="maas",
+ zone="zone1",
+ )
+
+ def test__create_no_optional(self):
+ origin = make_origin()
+ Devices = origin.Devices
+ Devices._handler.create.return_value = {}
+ observed = Devices.create(["00:11:22:33:44:55", "00:11:22:33:44:AA"])
+ self.assertThat(observed, IsInstance(devices.Device))
+ Devices._handler.create.assert_called_once_with(
+ mac_addresses=["00:11:22:33:44:55", "00:11:22:33:44:AA"]
+ )
def test__read(self):
data = {
diff --git a/maas/client/viscera/tests/test_dnsresourcerecords.py b/maas/client/viscera/tests/test_dnsresourcerecords.py
new file mode 100644
index 00000000..aa6045f4
--- /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 00000000..fc197eca
--- /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_domains.py b/maas/client/viscera/tests/test_domains.py
new file mode 100644
index 00000000..ce256021
--- /dev/null
+++ b/maas/client/viscera/tests/test_domains.py
@@ -0,0 +1,87 @@
+"""Tests for `maas.client.viscera.domains`."""
+
+import random
+
+from testtools.matchers import Equals, IsInstance, MatchesStructure
+
+from .. import domains
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Domain and Domains. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(domains.Domains, domains.Domain)
+
+
+class TestDomains(TestCase):
+ def test__domains_create(self):
+ origin = make_origin()
+ domain_id = random.randint(0, 100)
+ ttl = random.randint(0, 100)
+ name = make_string_without_spaces()
+ origin.Domains._handler.create.return_value = {
+ "id": domain_id,
+ "name": name,
+ "ttl": ttl,
+ "authoritative": False,
+ }
+ domain = origin.Domains.create(name=name, authoritative=False, ttl=ttl)
+ origin.Domains._handler.create.assert_called_once_with(
+ name=name, authoritative=False, ttl=ttl
+ )
+ self.assertThat(domain, IsInstance(origin.Domain))
+ self.assertThat(
+ domain,
+ MatchesStructure.byEquality(
+ id=domain_id, name=name, ttl=ttl, authoritative=False
+ ),
+ )
+
+ def test__domains_create_without_ttl(self):
+ origin = make_origin()
+ domain_id = random.randint(0, 100)
+ name = make_string_without_spaces()
+ origin.Domains._handler.create.return_value = {"id": domain_id, "name": name}
+ domain = origin.Domains.create(name=name)
+ origin.Domains._handler.create.assert_called_once_with(
+ name=name, authoritative=True
+ )
+ self.assertThat(domain, IsInstance(origin.Domain))
+ self.assertThat(domain, MatchesStructure.byEquality(id=domain_id, name=name))
+
+ def test__domains_read(self):
+ """Domains.read() returns a list of Domains."""
+ Domains = make_origin().Domains
+ domains = [
+ {"id": random.randint(0, 100), "name": make_string_without_spaces()}
+ for _ in range(3)
+ ]
+ Domains._handler.read.return_value = domains
+ domains = Domains.read()
+ self.assertThat(len(domains), Equals(3))
+
+
+class TestDomain(TestCase):
+ def test__domain_read(self):
+ Domain = make_origin().Domain
+ domain = {"id": random.randint(0, 100), "name": make_string_without_spaces()}
+ Domain._handler.read.return_value = domain
+ self.assertThat(Domain.read(id=domain["id"]), Equals(Domain(domain)))
+ Domain._handler.read.assert_called_once_with(id=domain["id"])
+
+ def test__domain_delete(self):
+ Domain = make_origin().Domain
+ domain = Domain(
+ {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "description": make_string_without_spaces(),
+ }
+ )
+ domain.delete()
+ Domain._handler.delete.assert_called_once_with(id=domain.id)
diff --git a/maas/client/viscera/tests/test_events.py b/maas/client/viscera/tests/test_events.py
index 00d38bb3..db9512df 100644
--- a/maas/client/viscera/tests/test_events.py
+++ b/maas/client/viscera/tests/test_events.py
@@ -1,27 +1,15 @@
"""Test for `maas.client.viscera.events`."""
-__all__ = []
-
from datetime import datetime
-from itertools import (
- chain,
- count,
-)
+from itertools import chain, count
import random
from unittest.mock import sentinel
-from testtools.matchers import (
- Equals,
- IsInstance,
-)
+from testtools.matchers import Equals, IsInstance
+from ..users import User
from .. import events
-from ...testing import (
- make_mac_address,
- make_name_without_spaces,
- randrange,
- TestCase,
-)
+from ...testing import make_mac_address, make_name_without_spaces, make_range, TestCase
from ..testing import bind
@@ -37,6 +25,7 @@ def make_Event_dict():
"level": random.choice(list(events.Level)),
"created": datetime.utcnow().strftime("%a, %d %b. %Y %H:%M:%S"),
"description": make_name_without_spaces("description"),
+ "username": make_name_without_spaces("username"),
}
@@ -52,10 +41,7 @@ def make_origin():
def make_queried_events():
"""Mimic the object returned from a query."""
- return {
- "events": [], "prev_uri": sentinel.prev_uri,
- "next_uri": sentinel.next_uri,
- }
+ return {"events": [], "prev_uri": sentinel.prev_uri, "next_uri": sentinel.next_uri}
class TestEventsQuery(TestCase):
@@ -66,7 +52,7 @@ def test__query_without_arguments_results_in_empty_bones_query(self):
obj.query()
obj._handler.query.assert_called_once_with()
- def test__query_arguments_are_assembled_and_passed_to_bones_handler(self):
+ def test__query_arguments_are_assembled_and_passed_to_bones_handler1(self):
obj = make_origin().Events
arguments = {
"hostnames": (
@@ -81,10 +67,53 @@ def test__query_arguments_are_assembled_and_passed_to_bones_handler(self):
make_name_without_spaces("zone"),
make_name_without_spaces("zone"),
),
- "macs": (
- make_mac_address(),
- make_mac_address(),
+ "macs": (make_mac_address(), make_mac_address()),
+ "system_ids": (
+ make_name_without_spaces("system-id"),
+ make_name_without_spaces("system-id"),
+ ),
+ "agent_name": make_name_without_spaces("agent"),
+ "level": random.choice(list(events.Level)),
+ "limit": random.randrange(1, 1000),
+ "owner": make_name_without_spaces("username"),
+ }
+ obj.query(**arguments)
+ expected = {
+ "hostname": list(arguments["hostnames"]),
+ "domain": list(arguments["domains"]),
+ "zone": list(arguments["zones"]),
+ "mac_address": list(arguments["macs"]),
+ "id": list(arguments["system_ids"]),
+ "agent_name": [arguments["agent_name"]],
+ "level": [arguments["level"].name],
+ "limit": [str(arguments["limit"])],
+ "owner": [arguments["owner"]],
+ }
+ obj._handler.query.assert_called_once_with(**expected)
+
+ def test__query_arguments_are_assembled_and_passed_to_bones_handler2(self):
+ obj = make_origin().Events
+ user = User(
+ {
+ "username": make_name_without_spaces("username"),
+ "email": make_name_without_spaces("user@"),
+ "is_superuser": False,
+ }
+ )
+ arguments = {
+ "hostnames": (
+ make_name_without_spaces("hostname"),
+ make_name_without_spaces("hostname"),
+ ),
+ "domains": (
+ make_name_without_spaces("domain"),
+ make_name_without_spaces("domain"),
),
+ "zones": (
+ make_name_without_spaces("zone"),
+ make_name_without_spaces("zone"),
+ ),
+ "macs": (make_mac_address(), make_mac_address()),
"system_ids": (
make_name_without_spaces("system-id"),
make_name_without_spaces("system-id"),
@@ -92,6 +121,7 @@ def test__query_arguments_are_assembled_and_passed_to_bones_handler(self):
"agent_name": make_name_without_spaces("agent"),
"level": random.choice(list(events.Level)),
"limit": random.randrange(1, 1000),
+ "owner": user,
}
obj.query(**arguments)
expected = {
@@ -103,9 +133,37 @@ def test__query_arguments_are_assembled_and_passed_to_bones_handler(self):
"agent_name": [arguments["agent_name"]],
"level": [arguments["level"].name],
"limit": [str(arguments["limit"])],
+ "owner": [user.username],
}
obj._handler.query.assert_called_once_with(**expected)
+ def test__query_arguments_raises_error_for_invalid_owner(self):
+ obj = make_origin().Events
+ arguments = {
+ "hostnames": (
+ make_name_without_spaces("hostname"),
+ make_name_without_spaces("hostname"),
+ ),
+ "domains": (
+ make_name_without_spaces("domain"),
+ make_name_without_spaces("domain"),
+ ),
+ "zones": (
+ make_name_without_spaces("zone"),
+ make_name_without_spaces("zone"),
+ ),
+ "macs": (make_mac_address(), make_mac_address()),
+ "system_ids": (
+ make_name_without_spaces("system-id"),
+ make_name_without_spaces("system-id"),
+ ),
+ "agent_name": make_name_without_spaces("agent"),
+ "level": random.choice(list(events.Level)),
+ "limit": random.randrange(1, 1000),
+ "owner": random.randint(0, 10),
+ }
+ self.assertRaises(TypeError, obj.query, **arguments)
+
def test__query_level_is_normalised(self):
obj = make_origin().Events
for level in events.Level:
@@ -135,37 +193,41 @@ class TestEvents(TestCase):
def test__prev_requests_page_of_older_events(self):
obj = make_origin().Events
- evts = obj({
- "events": [],
- "prev_uri": "endpoint?before=100&limit=20&foo=abc",
- "next_uri": "endpoint?after=119&limit=20&foo=123",
- })
+ evts = obj(
+ {
+ "events": [],
+ "prev_uri": "endpoint?before=100&limit=20&foo=abc",
+ "next_uri": "endpoint?after=119&limit=20&foo=123",
+ }
+ )
self.assertThat(evts.prev(), IsInstance(events.Events))
evts._handler.query.assert_called_once_with(
- before=["100"], limit=["20"], foo=["abc"],
+ before=["100"], limit=["20"], foo=["abc"]
)
def test__next_requests_page_of_newer_events(self):
obj = make_origin().Events
- evts = obj({
- "events": [],
- "prev_uri": "endpoint?before=100&limit=20&foo=abc",
- "next_uri": "endpoint?after=119&limit=20&foo=123",
- })
+ evts = obj(
+ {
+ "events": [],
+ "prev_uri": "endpoint?before=100&limit=20&foo=abc",
+ "next_uri": "endpoint?after=119&limit=20&foo=123",
+ }
+ )
self.assertThat(evts.next(), IsInstance(events.Events))
evts._handler.query.assert_called_once_with(
- after=["119"], limit=["20"], foo=["123"],
+ after=["119"], limit=["20"], foo=["123"]
)
def test__forwards_returns_a_continuous_iterator(self):
pages = [
{
- "events": [make_Event_dict() for _ in randrange()],
+ "events": [make_Event_dict() for _ in make_range()],
"prev_uri": "?going=backwards",
"next_uri": "?going=forwards",
},
{
- "events": [make_Event_dict() for _ in randrange()],
+ "events": [make_Event_dict() for _ in make_range()],
"prev_uri": "?going=backwards",
"next_uri": "?going=forwards",
},
@@ -180,20 +242,22 @@ def test__forwards_returns_a_continuous_iterator(self):
obj._handler.query.side_effect = pages
self.assertThat(
[evt._data for evt in obj.query().forwards()],
- Equals(list(chain.from_iterable(
- reversed(page["events"]) for page in pages))))
+ Equals(
+ list(chain.from_iterable(reversed(page["events"]) for page in pages))
+ ),
+ )
# The query parameters in next_uri get passed through to bones.
obj._handler.query.assert_called_with(going=["forwards"])
def test__backwards_returns_a_continuous_iterator(self):
pages = [
{
- "events": [make_Event_dict() for _ in randrange()],
+ "events": [make_Event_dict() for _ in make_range()],
"prev_uri": "?going=backwards",
"next_uri": "?going=forwards",
},
{
- "events": [make_Event_dict() for _ in randrange()],
+ "events": [make_Event_dict() for _ in make_range()],
"prev_uri": "?going=backwards",
"next_uri": "?going=forwards",
},
@@ -208,7 +272,7 @@ def test__backwards_returns_a_continuous_iterator(self):
obj._handler.query.side_effect = pages
self.assertThat(
[evt._data for evt in obj.query().backwards()],
- Equals(list(chain.from_iterable(
- page["events"] for page in pages))))
+ Equals(list(chain.from_iterable(page["events"] for page in pages))),
+ )
# The query parameters in prev_uri get passed through to bones.
obj._handler.query.assert_called_with(going=["backwards"])
diff --git a/maas/client/viscera/tests/test_fabrics.py b/maas/client/viscera/tests/test_fabrics.py
new file mode 100644
index 00000000..abef4eaa
--- /dev/null
+++ b/maas/client/viscera/tests/test_fabrics.py
@@ -0,0 +1,113 @@
+"""Test for `maas.client.viscera.fabrics`."""
+
+import random
+
+from testtools.matchers import (
+ Equals,
+ IsInstance,
+ MatchesAll,
+ MatchesSetwise,
+ MatchesStructure,
+)
+
+from ...errors import CannotDelete
+from ..fabrics import Fabric, Fabrics
+from ..vlans import Vlan, Vlans
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Fabrics, Fabric, Vlans, and Vlan.
+ """
+ return bind(Fabrics, Fabric, Vlans, Vlan)
+
+
+class TestFabrics(TestCase):
+ def test__fabrics_create(self):
+ Fabrics = make_origin().Fabrics
+ name = make_string_without_spaces()
+ description = make_string_without_spaces()
+ class_type = make_string_without_spaces()
+ Fabrics._handler.create.return_value = {
+ "id": 1,
+ "name": name,
+ "description": description,
+ "class_type": class_type,
+ }
+ Fabrics.create(name=name, description=description, class_type=class_type)
+ Fabrics._handler.create.assert_called_once_with(
+ name=name, description=description, class_type=class_type
+ )
+
+ def test__fabrics_read(self):
+ """Fabrics.read() returns a list of Fabrics."""
+ Fabrics = make_origin().Fabrics
+ fabrics = [
+ {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "class_type": make_string_without_spaces(),
+ }
+ for _ in range(3)
+ ]
+ Fabrics._handler.read.return_value = fabrics
+ fabrics = Fabrics.read()
+ self.assertThat(len(fabrics), Equals(3))
+
+
+class TestFabric(TestCase):
+ def test__fabric_get_default(self):
+ Fabric = make_origin().Fabric
+ Fabric._handler.read.return_value = {
+ "id": 0,
+ "name": make_string_without_spaces(),
+ "class_type": make_string_without_spaces(),
+ }
+ Fabric.get_default()
+ Fabric._handler.read.assert_called_once_with(id=0)
+
+ def test__fabric_read(self):
+ Fabric = make_origin().Fabric
+ fabric = {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "class_type": make_string_without_spaces(),
+ "vlans": [{"id": 1}, {"id": 2}],
+ }
+ Fabric._handler.read.return_value = fabric
+ self.assertThat(Fabric.read(id=fabric["id"]), Equals(Fabric(fabric)))
+ Fabric._handler.read.assert_called_once_with(id=fabric["id"])
+ self.assertThat(
+ Fabric(fabric).vlans,
+ MatchesSetwise(
+ MatchesAll(IsInstance(Vlan), MatchesStructure.byEquality(id=1)),
+ MatchesAll(IsInstance(Vlan), MatchesStructure.byEquality(id=2)),
+ ),
+ )
+
+ def test__fabric_delete(self):
+ Fabric = make_origin().Fabric
+ fabric_id = random.randint(1, 100)
+ fabric = Fabric(
+ {
+ "id": fabric_id,
+ "name": make_string_without_spaces(),
+ "class_type": make_string_without_spaces(),
+ }
+ )
+ fabric.delete()
+ Fabric._handler.delete.assert_called_once_with(id=fabric_id)
+
+ def test__fabric_delete_default(self):
+ Fabric = make_origin().Fabric
+ fabric = Fabric(
+ {
+ "id": 0,
+ "name": make_string_without_spaces(),
+ "class_type": make_string_without_spaces(),
+ }
+ )
+ self.assertRaises(CannotDelete, fabric.delete)
diff --git a/maas/client/viscera/tests/test_files.py b/maas/client/viscera/tests/test_files.py
index 1777bdb3..887ad34b 100644
--- a/maas/client/viscera/tests/test_files.py
+++ b/maas/client/viscera/tests/test_files.py
@@ -1,19 +1,9 @@
"""Test for `maas.client.viscera.files`."""
-__all__ = []
-
-from testtools.matchers import (
- AllMatch,
- IsInstance,
- MatchesSetwise,
- MatchesStructure,
-)
+from testtools.matchers import AllMatch, IsInstance, MatchesSetwise, MatchesStructure
from .. import files
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
+from ...testing import make_name_without_spaces, TestCase
from ..testing import bind
@@ -40,10 +30,15 @@ def test__read(self):
self.assertEquals(2, len(resources))
self.assertThat(resources, IsInstance(origin.Files))
self.assertThat(resources, AllMatch(IsInstance(origin.File)))
- self.assertThat(resources, MatchesSetwise(*(
- MatchesStructure.byEquality(filename=entry["filename"])
- for entry in data
- )))
+ self.assertThat(
+ resources,
+ MatchesSetwise(
+ *(
+ MatchesStructure.byEquality(filename=entry["filename"])
+ for entry in data
+ )
+ ),
+ )
class TestFile(TestCase):
@@ -53,5 +48,5 @@ def test__read(self):
origin = make_origin()
data = {"filename": make_name_without_spaces()}
self.assertThat(
- origin.File(data), MatchesStructure.byEquality(
- filename=data["filename"]))
+ origin.File(data), MatchesStructure.byEquality(filename=data["filename"])
+ )
diff --git a/maas/client/viscera/tests/test_interfaces.py b/maas/client/viscera/tests/test_interfaces.py
new file mode 100644
index 00000000..2eeabd87
--- /dev/null
+++ b/maas/client/viscera/tests/test_interfaces.py
@@ -0,0 +1,1313 @@
+"""Test for `maas.client.viscera.interfaces`."""
+
+import random
+
+from testtools.matchers import (
+ Equals,
+ IsInstance,
+ MatchesAll,
+ MatchesSetwise,
+ MatchesStructure,
+)
+
+from ..fabrics import Fabric, Fabrics
+from ..interfaces import (
+ Interface,
+ Interfaces,
+ InterfaceDiscoveredLink,
+ InterfaceDiscoveredLinks,
+ InterfaceLink,
+ InterfaceLinks,
+)
+from ..nodes import Node, Nodes
+from ..subnets import Subnet, Subnets
+from ..vlans import Vlan, Vlans
+
+from ..testing import bind
+from ...enum import InterfaceType, LinkMode
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with required objects.
+ """
+ return bind(
+ Fabrics,
+ Fabric,
+ Interfaces,
+ Interface,
+ InterfaceLinks,
+ InterfaceLink,
+ InterfaceDiscoveredLinks,
+ InterfaceDiscoveredLink,
+ Nodes,
+ Node,
+ Subnets,
+ Subnet,
+ Vlans,
+ Vlan,
+ )
+
+
+class TestInterfaces(TestCase):
+ def test__read_bad_node_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(TypeError, Interfaces.read, random.randint(0, 100))
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__read_with_system_id(self):
+ Interfaces = make_origin().Interfaces
+ system_id = make_string_without_spaces()
+ interfaces = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ }
+ for _ in range(3)
+ ]
+ Interfaces._handler.read.return_value = interfaces
+ interfaces = Interfaces.read(system_id)
+ self.assertThat(len(interfaces), Equals(3))
+ Interfaces._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__read_with_Node(self):
+ origin = make_origin()
+ Interfaces, Node = origin.Interfaces, origin.Node
+ system_id = make_string_without_spaces()
+ node = Node(system_id)
+ interfaces = [
+ {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ }
+ for _ in range(3)
+ ]
+ Interfaces._handler.read.return_value = interfaces
+ interfaces = Interfaces.read(node)
+ self.assertThat(len(interfaces), Equals(3))
+ Interfaces._handler.read.assert_called_once_with(system_id=system_id)
+
+ def test__create_bad_node_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(TypeError, Interfaces.create, random.randint(0, 100))
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__create_bad_vlan_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ vlan=make_string_without_spaces(),
+ )
+ self.assertEquals("vlan must be a Vlan or int, not str", str(error))
+
+ def test__create_bad_interface_type_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ interface_type=make_string_without_spaces(),
+ )
+ self.assertEquals(
+ "interface_type must be an InterfaceType, not str", str(error)
+ )
+
+ def test__create_physical_requires_mac_address(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError, Interfaces.create, make_string_without_spaces()
+ )
+ self.assertEquals("mac_address required for physical interface", str(error))
+
+ def test__create_physical_with_all_values(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ mac_address = "00:11:22:33:44:55"
+ name = make_string_without_spaces()
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan = random.randint(1, 20)
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.PHYSICAL.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_physical.return_value = interface_data
+ nic = Interfaces.create(
+ node=system_id,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_physical.assert_called_once_with(
+ system_id=system_id,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+
+ def test__create_physical_with_objects(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ node = origin.Node(system_id)
+ mac_address = "00:11:22:33:44:55"
+ name = make_string_without_spaces()
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan_id = random.randint(1, 20)
+ vlan = origin.Vlan({"fabric_id": random.randint(1, 20), "id": vlan_id})
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.PHYSICAL.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_physical.return_value = interface_data
+ nic = Interfaces.create(
+ node=node,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_physical.assert_called_once_with(
+ system_id=system_id,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan_id,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+
+ def test__create_bond_fails_with_parent(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BOND,
+ name=make_string_without_spaces(),
+ parent=make_string_without_spaces(),
+ )
+ self.assertEquals("use parents not parent for bond interface", str(error))
+
+ def test__create_bond_fails_parents_not_iterable(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BOND,
+ name=make_string_without_spaces(),
+ parents=random.randint(1, 20),
+ )
+ self.assertEquals("parents must be a iterable, not int", str(error))
+
+ def test__create_bond_fails_parents_is_not_Interface_or_int(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BOND,
+ name=make_string_without_spaces(),
+ parents=[make_string_without_spaces()],
+ )
+ self.assertEquals("parent[0] must be an Interface or int, not str", str(error))
+
+ def test__create_bond_fails_name_missing(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BOND,
+ parents=[random.randint(1, 20)],
+ )
+ self.assertEquals("name is required for bond interface", str(error))
+
+ def test__create_bond_with_all_values(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ mac_address = "00:11:22:33:44:55"
+ name = make_string_without_spaces()
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan = random.randint(1, 20)
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ bond_mode = make_string_without_spaces()
+ bond_miimon = random.randint(1, 100)
+ bond_downdelay = random.randint(1, 10)
+ bond_updelay = random.randint(1, 10)
+ bond_lacp_rate = random.choice(["fast", "slow"])
+ bond_xmit_hash_policy = make_string_without_spaces()
+ parents = [random.randint(1, 10) for _ in range(3)]
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.BOND.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_bond.return_value = interface_data
+ nic = Interfaces.create(
+ node=system_id,
+ interface_type=InterfaceType.BOND,
+ parents=parents,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ bond_mode=bond_mode,
+ bond_miimon=bond_miimon,
+ bond_downdelay=bond_downdelay,
+ bond_updelay=bond_updelay,
+ bond_lacp_rate=bond_lacp_rate,
+ bond_xmit_hash_policy=bond_xmit_hash_policy,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_bond.assert_called_once_with(
+ system_id=system_id,
+ parents=parents,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ bond_mode=bond_mode,
+ bond_miimon=bond_miimon,
+ bond_downdelay=bond_downdelay,
+ bond_updelay=bond_updelay,
+ bond_lacp_rate=bond_lacp_rate,
+ bond_xmit_hash_policy=bond_xmit_hash_policy,
+ )
+
+ def test__create_bond_with_objects(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ node = origin.Node(system_id)
+ mac_address = "00:11:22:33:44:55"
+ name = make_string_without_spaces()
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan_id = random.randint(1, 10)
+ vlan = origin.Vlan({"fabric_id": random.randint(1, 20), "id": vlan_id})
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ bond_mode = make_string_without_spaces()
+ bond_miimon = random.randint(1, 100)
+ bond_downdelay = random.randint(1, 10)
+ bond_updelay = random.randint(1, 10)
+ bond_lacp_rate = random.choice(["fast", "slow"])
+ bond_xmit_hash_policy = make_string_without_spaces()
+ parents = [random.randint(1, 10) for _ in range(3)]
+ parent_objs = [Interface((system_id, parent)) for parent in parents]
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.BOND.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_bond.return_value = interface_data
+ nic = Interfaces.create(
+ node=node,
+ interface_type=InterfaceType.BOND,
+ parents=parent_objs,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ bond_mode=bond_mode,
+ bond_miimon=bond_miimon,
+ bond_downdelay=bond_downdelay,
+ bond_updelay=bond_updelay,
+ bond_lacp_rate=bond_lacp_rate,
+ bond_xmit_hash_policy=bond_xmit_hash_policy,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_bond.assert_called_once_with(
+ system_id=system_id,
+ parents=parents,
+ mac_address=mac_address,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan_id,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ bond_mode=bond_mode,
+ bond_miimon=bond_miimon,
+ bond_downdelay=bond_downdelay,
+ bond_updelay=bond_updelay,
+ bond_lacp_rate=bond_lacp_rate,
+ bond_xmit_hash_policy=bond_xmit_hash_policy,
+ )
+
+ def test__create_vlan_fails_with_parents(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.VLAN,
+ parents=[make_string_without_spaces()],
+ )
+ self.assertEquals("use parent not parents for VLAN interface", str(error))
+
+ def test__create_vlan_fails_without_parent(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.VLAN,
+ )
+ self.assertEquals("parent is required for VLAN interface", str(error))
+
+ def test__create_vlan_fails_parent_wrong_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.VLAN,
+ parent=make_string_without_spaces(),
+ )
+ self.assertEquals("parent must be an Interface or int, not str", str(error))
+
+ def test__create_vlan_fails_without_vlan(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.VLAN,
+ parent=random.randint(1, 10),
+ )
+ self.assertEquals("vlan is required for VLAN interface", str(error))
+
+ def test__create_vlan_with_all_values(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan = random.randint(1, 20)
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ parent = random.randint(1, 10)
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.VLAN.value,
+ "tags": tags,
+ }
+ Interfaces._handler.create_vlan.return_value = interface_data
+ nic = Interfaces.create(
+ node=system_id,
+ interface_type=InterfaceType.VLAN,
+ parent=parent,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_vlan.assert_called_once_with(
+ system_id=system_id,
+ parent=parent,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+
+ def test__create_vlan_with_objects(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ node = origin.Node(system_id)
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan_id = random.randint(1, 10)
+ vlan = origin.Vlan({"fabric_id": random.randint(1, 20), "id": vlan_id})
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ parent_id = random.randint(1, 10)
+ parent_obj = Interface((system_id, parent_id))
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.VLAN.value,
+ "tags": tags,
+ }
+ Interfaces._handler.create_vlan.return_value = interface_data
+ nic = Interfaces.create(
+ node=node,
+ interface_type=InterfaceType.VLAN,
+ parent=parent_obj,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_vlan.assert_called_once_with(
+ system_id=system_id,
+ parent=parent_id,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan_id,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ )
+
+ def test__create_bridge_fails_with_parents(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BRIDGE,
+ parents=[make_string_without_spaces()],
+ )
+ self.assertEquals("use parent not parents for bridge interface", str(error))
+
+ def test__create_bridge_fails_without_parent(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BRIDGE,
+ )
+ self.assertEquals("parent is required for bridge interface", str(error))
+
+ def test__create_bridge_fails_parent_wrong_type(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ TypeError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BRIDGE,
+ parent=make_string_without_spaces(),
+ )
+ self.assertEquals("parent must be an Interface or int, not str", str(error))
+
+ def test__create_bridge_fails_missing_name(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.BRIDGE,
+ parent=random.randint(1, 10),
+ )
+ self.assertEquals("name is required for bridge interface", str(error))
+
+ def test__create_bridge_with_all_values(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ name = make_string_without_spaces()
+ mac_address = "00:11:22:33:44:55"
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan = random.randint(1, 20)
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ parent = random.randint(1, 10)
+ bridge_stp = random.choice([True, False])
+ bridge_fd = random.randint(1, 10)
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.BRIDGE.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_bridge.return_value = interface_data
+ nic = Interfaces.create(
+ node=system_id,
+ interface_type=InterfaceType.BRIDGE,
+ parent=parent,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ mac_address=mac_address,
+ bridge_stp=bridge_stp,
+ bridge_fd=bridge_fd,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_bridge.assert_called_once_with(
+ system_id=system_id,
+ parent=parent,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ mac_address=mac_address,
+ bridge_stp=bridge_stp,
+ bridge_fd=bridge_fd,
+ )
+
+ def test__create_bridge_with_objects(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ node = origin.Node(system_id)
+ name = make_string_without_spaces()
+ mac_address = "00:11:22:33:44:55"
+ tags = [make_string_without_spaces() for _ in range(3)]
+ mtu = random.randint(1500, 3000)
+ vlan_id = random.randint(1, 10)
+ vlan = origin.Vlan({"fabric_id": random.randint(1, 20), "id": vlan_id})
+ accept_ra = random.choice([True, False])
+ autoconf = random.choice([True, False])
+ parent_id = random.randint(1, 10)
+ parent_obj = Interface((system_id, parent_id))
+ bridge_stp = random.choice([True, False])
+ bridge_fd = random.randint(1, 10)
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(1, 20),
+ "type": InterfaceType.VLAN.value,
+ "name": name,
+ "tags": tags,
+ }
+ Interfaces._handler.create_bridge.return_value = interface_data
+ nic = Interfaces.create(
+ node=node,
+ interface_type=InterfaceType.BRIDGE,
+ parent=parent_obj,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ mac_address=mac_address,
+ bridge_stp=bridge_stp,
+ bridge_fd=bridge_fd,
+ )
+ self.assertThat(nic, IsInstance(Interface))
+ Interfaces._handler.create_bridge.assert_called_once_with(
+ system_id=system_id,
+ parent=parent_id,
+ name=name,
+ tags=tags,
+ mtu=mtu,
+ vlan=vlan_id,
+ accept_ra=accept_ra,
+ autoconf=autoconf,
+ mac_address=mac_address,
+ bridge_stp=bridge_stp,
+ bridge_fd=bridge_fd,
+ )
+
+ def test__create_unknown_fails(self):
+ Interfaces = make_origin().Interfaces
+ error = self.assertRaises(
+ ValueError,
+ Interfaces.create,
+ make_string_without_spaces(),
+ InterfaceType.UNKNOWN,
+ )
+ self.assertEquals(
+ "cannot create an interface of type: %s" % InterfaceType.UNKNOWN, str(error)
+ )
+
+ def test__by_name(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ eth0_Interface = Interface(
+ {"system_id": system_id, "id": random.randint(0, 100), "name": "eth0"}
+ )
+ eth1_Interface = Interface(
+ {"system_id": system_id, "id": random.randint(0, 100), "name": "eth1"}
+ )
+ interfaces = Interfaces([eth0_Interface, eth1_Interface])
+ self.assertEquals(
+ {"eth0": eth0_Interface, "eth1": eth1_Interface}, interfaces.by_name
+ )
+
+ def test__get_by_name(self):
+ origin = make_origin()
+ Interfaces, Interface = origin.Interfaces, origin.Interface
+ system_id = make_string_without_spaces()
+ eth0_Interface = Interface(
+ {"system_id": system_id, "id": random.randint(0, 100), "name": "eth0"}
+ )
+ eth1_Interface = Interface(
+ {"system_id": system_id, "id": random.randint(0, 100), "name": "eth1"}
+ )
+ interfaces = Interfaces([eth0_Interface, eth1_Interface])
+ eth0 = interfaces.get_by_name("eth0")
+ self.assertEquals(eth0, eth0_Interface)
+
+
+class TestInterface(TestCase):
+ def test__interface_read_system_id(self):
+ Interface = make_origin().Interface
+ system_id = make_string_without_spaces()
+ interface = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ }
+ Interface._handler.read.return_value = interface
+ self.assertThat(
+ Interface.read(node=system_id, id=interface["id"]),
+ Equals(Interface(interface)),
+ )
+ Interface._handler.read.assert_called_once_with(
+ system_id=system_id, id=interface["id"]
+ )
+
+ def test__interface_read_Node(self):
+ origin = make_origin()
+ Interface = origin.Interface
+ system_id = make_string_without_spaces()
+ interface = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ }
+ Interface._handler.read.return_value = interface
+ self.assertThat(
+ Interface.read(node=origin.Node(system_id), id=interface["id"]),
+ Equals(Interface(interface)),
+ )
+ Interface._handler.read.assert_called_once_with(
+ system_id=system_id, id=interface["id"]
+ )
+
+ def test__interface_read_TypeError(self):
+ Interface = make_origin().Interface
+ error = self.assertRaises(
+ TypeError, Interface.read, random.randint(0, 100), random.randint(0, 100)
+ )
+ self.assertEquals("node must be a Node or str, not int", str(error))
+
+ def test__interface_save_tags(self):
+ Interface = make_origin().Interface
+ Interface._handler.params = ["system_id", "id"]
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": ["keep", "update", "delete"],
+ "params": {},
+ }
+ )
+ del interface.tags[2]
+ interface.tags[1] = "updated"
+ Interface._handler.update.return_value = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "tags": ["keep", "updated"],
+ "params": {},
+ }
+ interface.save()
+ Interface._handler.update.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, tags="keep,updated"
+ )
+
+ def test__interface_doesnt_save_tags_if_same_diff_order(self):
+ Interface = make_origin().Interface
+ Interface._handler.params = ["system_id", "id"]
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": ["keep", "update", "delete"],
+ "params": {},
+ }
+ )
+ interface.tags = ["delete", "keep", "update"]
+ interface.save()
+ self.assertEqual(0, Interface._handler.update.call_count)
+
+ def test__interface_save_passes_parameters(self):
+ Interface = make_origin().Interface
+ Interface._handler.params = ["system_id", "id"]
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": [],
+ "params": {"mtu": 1500},
+ }
+ )
+ interface.params["mtu"] = 3000
+ Interface._handler.update.return_value = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "tags": [],
+ "params": {"mtu": 3000},
+ }
+ interface.save()
+ Interface._handler.update.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, mtu=3000
+ )
+
+ def test__interface_save_handles_str_params(self):
+ Interface = make_origin().Interface
+ Interface._handler.params = ["system_id", "id"]
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": [],
+ "params": "",
+ }
+ )
+ interface.params = {"mtu": 3000}
+ Interface._handler.update.return_value = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "tags": [],
+ "params": {"mtu": 3000},
+ }
+ interface.save()
+ Interface._handler.update.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, mtu=3000
+ )
+
+ def test__interface_save_sets_vlan_to_None(self):
+ Interface = make_origin().Interface
+ Interface._handler.params = ["system_id", "id"]
+ vlan_data = {
+ "fabric_id": random.randint(0, 100),
+ "id": random.randint(0, 100),
+ "vid": random.randint(0, 100),
+ }
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": [],
+ "params": {},
+ "vlan": vlan_data,
+ }
+ )
+ interface.vlan = None
+ Interface._handler.update.return_value = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "tags": [],
+ "params": {},
+ "vlan": None,
+ }
+ interface.save()
+ Interface._handler.update.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, vlan=None
+ )
+
+ def test__interface_save_sets_vlan_to_new_vlan(self):
+ origin = make_origin()
+ Interface, Vlan = origin.Interface, origin.Vlan
+ Interface._handler.params = ["system_id", "id"]
+ vlan_data = {
+ "fabric_id": random.randint(0, 100),
+ "id": random.randint(0, 100),
+ "vid": random.randint(0, 100),
+ }
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": [],
+ "params": {},
+ "vlan": vlan_data,
+ }
+ )
+ new_vlan = Vlan(
+ {
+ "fabric_id": random.randint(0, 100),
+ "id": random.randint(101, 200),
+ "vid": random.randint(0, 100),
+ }
+ )
+ interface.vlan = new_vlan
+ Interface._handler.update.return_value = {
+ "system_id": interface.node.system_id,
+ "id": interface.id,
+ "tags": [],
+ "params": {},
+ "vlan": new_vlan._data,
+ }
+ interface.save()
+ Interface._handler.update.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, vlan=new_vlan.id
+ )
+
+ def test__interface_save_doesnt_change_vlan_when_same(self):
+ origin = make_origin()
+ Interface, Vlan = origin.Interface, origin.Vlan
+ Interface._handler.params = ["system_id", "id"]
+ vlan_data = {
+ "fabric_id": random.randint(0, 100),
+ "id": random.randint(0, 100),
+ "vid": random.randint(0, 100),
+ }
+ vlan = Vlan(dict(vlan_data))
+ interface = Interface(
+ {
+ "system_id": make_string_without_spaces(),
+ "id": random.randint(0, 100),
+ "tags": [],
+ "params": {},
+ "vlan": vlan_data,
+ }
+ )
+ interface.vlan = vlan
+ interface.save()
+ self.assertEqual(0, Interface._handler.update.call_count)
+
+ def test__interface_delete(self):
+ Interface = make_origin().Interface
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ }
+ interface = Interface(interface_data)
+ interface.delete()
+ Interface._handler.delete.assert_called_once_with(
+ system_id=system_id, id=interface_data["id"]
+ )
+
+ def test__interface_disconnect(self):
+ Interface = make_origin().Interface
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [{"id": random.randint(0, 100), "mode": LinkMode.AUTO.value}],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ updated_data["links"] = []
+ Interface._handler.disconnect.return_value = updated_data
+ interface.disconnect()
+ Interface._handler.disconnect.assert_called_once_with(
+ system_id=system_id, id=interface_data["id"]
+ )
+ self.assertEquals([], list(interface.links))
+
+ def test__interface_links_create_raises_TypeError_no_Interface(self):
+ origin = make_origin()
+ InterfaceLinks = origin.InterfaceLinks
+ error = self.assertRaises(
+ TypeError, InterfaceLinks.create, random.randint(0, 1000), LinkMode.AUTO
+ )
+ self.assertEquals("interface must be an Interface, not int", str(error))
+
+ def test__interface_links_create_raises_TypeError_no_LinkMode(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ TypeError, InterfaceLinks.create, interface, LinkMode.AUTO.value
+ )
+ self.assertEquals("mode must be a LinkMode, not str", str(error))
+
+ def test__interface_links_create_raises_TypeError_no_Subnet(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ TypeError,
+ InterfaceLinks.create,
+ interface,
+ LinkMode.AUTO,
+ subnet=make_string_without_spaces(),
+ )
+ self.assertEquals("subnet must be a Subnet or int, not str", str(error))
+
+ def test__interface_links_create_raises_ValueError_AUTO_no_Subnet(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ ValueError, InterfaceLinks.create, interface, LinkMode.AUTO
+ )
+ self.assertEquals("subnet is required for %s" % LinkMode.AUTO, str(error))
+
+ def test__interface_links_create_raises_ValueError_STATIC_no_Subnet(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ ValueError, InterfaceLinks.create, interface, LinkMode.STATIC
+ )
+ self.assertEquals("subnet is required for %s" % LinkMode.STATIC, str(error))
+
+ def test__interface_links_create_raises_ValueError_LINK_UP_gateway(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ ValueError,
+ InterfaceLinks.create,
+ interface,
+ LinkMode.LINK_UP,
+ default_gateway=True,
+ )
+ self.assertEquals(
+ "cannot set as default_gateway for %s" % LinkMode.LINK_UP, str(error)
+ )
+
+ def test__interface_links_create_raises_ValueError_DHCP_gateway(self):
+ origin = make_origin()
+ Interface, InterfaceLinks = origin.Interface, origin.InterfaceLinks
+ interface = Interface(
+ {"system_id": make_string_without_spaces(), "id": random.randint(0, 100)}
+ )
+ error = self.assertRaises(
+ ValueError,
+ InterfaceLinks.create,
+ interface,
+ LinkMode.DHCP,
+ default_gateway=True,
+ )
+ self.assertEquals(
+ "cannot set as default_gateway for %s" % LinkMode.DHCP, str(error)
+ )
+
+ def test__interface_links_create_AUTO(self):
+ origin = make_origin()
+ Interface, Subnet = origin.Interface, origin.Subnet
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [
+ {
+ "id": random.randint(0, 100),
+ "mode": LinkMode.LINK_UP.value,
+ "subnet": {"id": random.randint(0, 100)},
+ }
+ ],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ link_id = random.randint(100, 200)
+ subnet_id = random.randint(1, 100)
+ updated_data["links"] = [
+ {"id": link_id, "mode": LinkMode.AUTO.value, "subnet": {"id": subnet_id}}
+ ]
+ Interface._handler.link_subnet.return_value = updated_data
+ interface.links.create(LinkMode.AUTO, subnet_id)
+ Interface._handler.link_subnet.assert_called_once_with(
+ system_id=interface.node.system_id,
+ id=interface.id,
+ mode=LinkMode.AUTO.value,
+ subnet=subnet_id,
+ force=False,
+ default_gateway=False,
+ )
+ self.assertThat(
+ interface.links,
+ MatchesSetwise(
+ MatchesStructure(
+ id=Equals(link_id),
+ mode=Equals(LinkMode.AUTO),
+ subnet=MatchesAll(
+ IsInstance(Subnet), MatchesStructure(id=Equals(subnet_id))
+ ),
+ )
+ ),
+ )
+
+ def test__interface_links_create_STATIC(self):
+ origin = make_origin()
+ Interface, Subnet = origin.Interface, origin.Subnet
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [
+ {
+ "id": random.randint(0, 100),
+ "mode": LinkMode.LINK_UP.value,
+ "subnet": {"id": random.randint(0, 100)},
+ }
+ ],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ link_id = random.randint(100, 200)
+ subnet_id = random.randint(1, 100)
+ updated_data["links"] = [
+ {
+ "id": link_id,
+ "mode": LinkMode.STATIC.value,
+ "ip_address": "192.168.122.10",
+ "subnet": {"id": subnet_id},
+ }
+ ]
+ Interface._handler.link_subnet.return_value = updated_data
+ interface.links.create(
+ LinkMode.STATIC,
+ subnet=Subnet(subnet_id),
+ ip_address="192.168.122.10",
+ default_gateway=True,
+ force=True,
+ )
+ Interface._handler.link_subnet.assert_called_once_with(
+ system_id=interface.node.system_id,
+ id=interface.id,
+ mode=LinkMode.STATIC.value,
+ subnet=subnet_id,
+ ip_address="192.168.122.10",
+ force=True,
+ default_gateway=True,
+ )
+ self.assertThat(
+ interface.links,
+ MatchesSetwise(
+ MatchesStructure(
+ id=Equals(link_id),
+ mode=Equals(LinkMode.STATIC),
+ ip_address=Equals("192.168.122.10"),
+ subnet=MatchesAll(
+ IsInstance(Subnet), MatchesStructure(id=Equals(subnet_id))
+ ),
+ )
+ ),
+ )
+
+ def test__interface_links_create_DHCP(self):
+ origin = make_origin()
+ Interface, Subnet = origin.Interface, origin.Subnet
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [
+ {
+ "id": random.randint(0, 100),
+ "mode": LinkMode.LINK_UP.value,
+ "subnet": {"id": random.randint(0, 100)},
+ }
+ ],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ link_id = random.randint(100, 200)
+ subnet_id = random.randint(1, 100)
+ updated_data["links"] = [
+ {"id": link_id, "mode": LinkMode.DHCP.value, "subnet": {"id": subnet_id}}
+ ]
+ Interface._handler.link_subnet.return_value = updated_data
+ interface.links.create(LinkMode.DHCP, subnet=Subnet(subnet_id))
+ Interface._handler.link_subnet.assert_called_once_with(
+ system_id=interface.node.system_id,
+ id=interface.id,
+ mode=LinkMode.DHCP.value,
+ subnet=subnet_id,
+ default_gateway=False,
+ force=False,
+ )
+ self.assertThat(
+ interface.links,
+ MatchesSetwise(
+ MatchesStructure(
+ id=Equals(link_id),
+ mode=Equals(LinkMode.DHCP),
+ subnet=MatchesAll(
+ IsInstance(Subnet), MatchesStructure(id=Equals(subnet_id))
+ ),
+ )
+ ),
+ )
+
+ def test__interface_links_create_LINK_UP(self):
+ origin = make_origin()
+ Interface, Subnet = origin.Interface, origin.Subnet
+ system_id = make_string_without_spaces()
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ link_id = random.randint(0, 100)
+ subnet_id = random.randint(1, 100)
+ updated_data["links"] = [
+ {"id": link_id, "mode": LinkMode.LINK_UP.value, "subnet": {"id": subnet_id}}
+ ]
+ Interface._handler.link_subnet.return_value = updated_data
+ interface.links.create(LinkMode.LINK_UP, subnet=Subnet(subnet_id))
+ Interface._handler.link_subnet.assert_called_once_with(
+ system_id=interface.node.system_id,
+ id=interface.id,
+ mode=LinkMode.LINK_UP.value,
+ subnet=subnet_id,
+ default_gateway=False,
+ force=False,
+ )
+ self.assertThat(
+ interface.links,
+ MatchesSetwise(
+ MatchesStructure(
+ id=Equals(link_id),
+ mode=Equals(LinkMode.LINK_UP),
+ subnet=MatchesAll(
+ IsInstance(Subnet), MatchesStructure(id=Equals(subnet_id))
+ ),
+ )
+ ),
+ )
+
+ def test__interface_links_delete(self):
+ origin = make_origin()
+ Interface, Subnet = origin.Interface, origin.Subnet
+ system_id = make_string_without_spaces()
+ link_id = random.randint(0, 100)
+ subnet_id = random.randint(1, 100)
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [
+ {
+ "id": link_id,
+ "mode": LinkMode.AUTO.value,
+ "subnet": {"id": subnet_id},
+ }
+ ],
+ }
+ interface = Interface(interface_data)
+ updated_data = dict(interface_data)
+ new_link_id = random.randint(0, 100)
+ new_subnet_id = random.randint(1, 100)
+ updated_data["links"] = [
+ {
+ "id": new_link_id,
+ "mode": LinkMode.LINK_UP.value,
+ "subnet": {"id": new_subnet_id},
+ }
+ ]
+ Interface._handler.unlink_subnet.return_value = updated_data
+ interface.links[0].delete()
+ Interface._handler.unlink_subnet.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, _id=link_id
+ )
+ self.assertThat(
+ interface.links,
+ MatchesSetwise(
+ MatchesStructure(
+ id=Equals(new_link_id),
+ mode=Equals(LinkMode.LINK_UP),
+ subnet=MatchesAll(
+ IsInstance(Subnet), MatchesStructure(id=Equals(new_subnet_id))
+ ),
+ )
+ ),
+ )
+
+ def test__interface_links_set_as_default_gateway(self):
+ origin = make_origin()
+ Interface = origin.Interface
+ system_id = make_string_without_spaces()
+ link_id = random.randint(0, 100)
+ subnet_id = random.randint(1, 100)
+ interface_data = {
+ "system_id": system_id,
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "type": InterfaceType.PHYSICAL.value,
+ "links": [
+ {
+ "id": link_id,
+ "mode": LinkMode.AUTO.value,
+ "subnet": {"id": subnet_id},
+ }
+ ],
+ }
+ interface = Interface(interface_data)
+ interface.links[0].set_as_default_gateway()
+ Interface._handler.set_default_gateway.assert_called_once_with(
+ system_id=interface.node.system_id, id=interface.id, link_id=link_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 00000000..a5313e6b
--- /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/maas/client/viscera/tests/test_ipranges.py b/maas/client/viscera/tests/test_ipranges.py
new file mode 100644
index 00000000..5732da9f
--- /dev/null
+++ b/maas/client/viscera/tests/test_ipranges.py
@@ -0,0 +1,101 @@
+"""Test for `maas.client.viscera.ipranges`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..ipranges import IPRange, IPRanges
+
+from ..testing import bind
+from ...enum import IPRangeType
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with IPRanges and IPRange. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(IPRanges, IPRange)
+
+
+class TestIPRanges(TestCase):
+ def test__ipranges_create(self):
+ IPRanges = make_origin().IPRanges
+ start_ip = make_string_without_spaces()
+ end_ip = make_string_without_spaces()
+ type = IPRangeType.DYNAMIC
+ comment = make_string_without_spaces()
+ IPRanges._handler.create.return_value = {
+ "id": 1,
+ "start_ip": start_ip,
+ "end_ip": end_ip,
+ "type": type.value,
+ "comment": comment,
+ }
+ IPRanges.create(start_ip=start_ip, end_ip=end_ip, type=type, comment=comment)
+ IPRanges._handler.create.assert_called_once_with(
+ start_ip=start_ip, end_ip=end_ip, type=type.value, comment=comment
+ )
+
+ def test__ipranges_create_requires_IPRangeType(self):
+ IPRanges = make_origin().IPRanges
+ start_ip = make_string_without_spaces()
+ end_ip = make_string_without_spaces()
+ comment = make_string_without_spaces()
+ error = self.assertRaises(
+ TypeError,
+ IPRanges.create,
+ start_ip=start_ip,
+ end_ip=end_ip,
+ type=make_string_without_spaces(),
+ comment=comment,
+ )
+ self.assertEquals("type must be an IPRangeType, not str", str(error))
+
+ def test__ipranges_read(self):
+ """IPRanges.read() returns a list of IPRanges."""
+ IPRanges = make_origin().IPRanges
+ ipranges = [
+ {
+ "id": random.randint(0, 100),
+ "start_ip": make_string_without_spaces(),
+ "end_ip": make_string_without_spaces(),
+ "type": make_string_without_spaces(),
+ "comment": make_string_without_spaces(),
+ }
+ for _ in range(3)
+ ]
+ IPRanges._handler.read.return_value = ipranges
+ ipranges = IPRanges.read()
+ self.assertThat(len(ipranges), Equals(3))
+
+
+class TestIPRange(TestCase):
+ def test__iprange_read(self):
+ IPRange = make_origin().IPRange
+ iprange = {
+ "id": random.randint(0, 100),
+ "start_ip": make_string_without_spaces(),
+ "end_ip": make_string_without_spaces(),
+ "type": make_string_without_spaces(),
+ "comment": make_string_without_spaces(),
+ }
+ IPRange._handler.read.return_value = iprange
+ self.assertThat(IPRange.read(id=iprange["id"]), Equals(IPRange(iprange)))
+ IPRange._handler.read.assert_called_once_with(id=iprange["id"])
+
+ def test__iprange_delete(self):
+ IPRange = make_origin().IPRange
+ iprange_id = random.randint(1, 100)
+ iprange = IPRange(
+ {
+ "id": iprange_id,
+ "start_ip": make_string_without_spaces(),
+ "end_ip": make_string_without_spaces(),
+ "type": make_string_without_spaces(),
+ "comment": make_string_without_spaces(),
+ }
+ )
+ iprange.delete()
+ IPRange._handler.delete.assert_called_once_with(id=iprange_id)
diff --git a/maas/client/viscera/tests/test_logical_volumes.py b/maas/client/viscera/tests/test_logical_volumes.py
new file mode 100644
index 00000000..3c54c989
--- /dev/null
+++ b/maas/client/viscera/tests/test_logical_volumes.py
@@ -0,0 +1,72 @@
+"""Test for `maas.client.viscera.logical_volumes`."""
+
+import random
+from testtools.matchers import Equals, IsInstance
+
+from .. import block_devices, logical_volumes, nodes, volume_groups
+from ...testing import make_name_without_spaces, TestCase
+from ..testing import bind
+
+
+def make_origin():
+ # Create a new origin with Devices and Device. The former refers to the
+ # latter via the origin, hence why it must be bound.
+ return bind(
+ logical_volumes.LogicalVolume,
+ logical_volumes.LogicalVolumes,
+ block_devices.BlockDevice,
+ block_devices.BlockDevices,
+ nodes.Node,
+ volume_groups.VolumeGroup,
+ )
+
+
+class TestLogicalVolume(TestCase):
+ def test__string_representation_includes_only_name(self):
+ volume = logical_volumes.LogicalVolume(
+ {"name": make_name_without_spaces("name")}
+ )
+ self.assertThat(
+ repr(volume), Equals("" % volume._data)
+ )
+
+
+class TestLogicalVolumes(TestCase):
+ def test__create(self):
+ origin = make_origin()
+
+ system_id = make_name_without_spaces("system-id")
+ lv_id = random.randint(1, 20)
+ lv_name = make_name_without_spaces("lvname")
+
+ VolumeGroup = origin.VolumeGroup
+ vg = VolumeGroup({"system_id": system_id, "id": random.randint(21, 30)})
+ VolumeGroup._handler.create_logical_volume.return_value = {
+ "system_id": system_id,
+ "id": lv_id,
+ }
+
+ uuid = make_name_without_spaces("uuid")
+ tags = [make_name_without_spaces("tag")]
+
+ BlockDevice = origin.BlockDevice
+ BlockDevice._handler.read.return_value = {
+ "system_id": system_id,
+ "id": lv_id,
+ "name": lv_name,
+ "tags": [],
+ "uuid": uuid,
+ }
+
+ LogicalVolumes = origin.LogicalVolumes
+ observed = LogicalVolumes.create(vg, lv_name, 10 * 1024, uuid=uuid, tags=tags)
+ self.assertThat(observed, IsInstance(logical_volumes.LogicalVolume))
+ self.assertThat(observed.name, Equals(lv_name))
+
+ VolumeGroup._handler.create_logical_volume.assert_called_once_with(
+ system_id=system_id, id=vg.id, name=lv_name, size=10 * 1024, uuid=uuid
+ )
+ BlockDevice._handler.read.assert_called_once_with(system_id=system_id, id=lv_id)
+ BlockDevice._handler.add_tag.assert_called_once_with(
+ system_id=system_id, id=lv_id, tag=tags[0]
+ )
diff --git a/maas/client/viscera/tests/test_maas.py b/maas/client/viscera/tests/test_maas.py
new file mode 100644
index 00000000..5d1a28a2
--- /dev/null
+++ b/maas/client/viscera/tests/test_maas.py
@@ -0,0 +1,31 @@
+"""Tests for MAAS configuration and suchlike."""
+
+from testtools.matchers import HasLength
+
+from .. import maas
+from ...testing import TestCase
+
+
+def find_getters(cls):
+ return {
+ name[4:]: getattr(cls, name)
+ for name in dir(cls)
+ if name.startswith("get_") and name != "get_config"
+ }
+
+
+def find_setters(cls):
+ return {
+ name[4:]: getattr(cls, name)
+ for name in dir(cls)
+ if name.startswith("set_") and name != "set_config"
+ }
+
+
+class TestConfiguration(TestCase):
+ def test__every_getter_has_a_setter_and_vice_versa(self):
+ getters, setters = find_getters(maas.MAAS), find_setters(maas.MAAS)
+ getters_without_setters = set(getters).difference(setters)
+ self.assertThat(getters_without_setters, HasLength(0))
+ setters_without_getters = set(setters).difference(getters)
+ self.assertThat(setters_without_getters, HasLength(0))
diff --git a/maas/client/viscera/tests/test_machines.py b/maas/client/viscera/tests/test_machines.py
index 655bffac..62d97ad5 100644
--- a/maas/client/viscera/tests/test_machines.py
+++ b/maas/client/viscera/tests/test_machines.py
@@ -1,70 +1,1063 @@
"""Test for `maas.client.viscera.machines`."""
-__all__ = []
+import random
+from http import HTTPStatus
+from unittest.mock import Mock
+from xml.etree import ElementTree
-from testtools.matchers import Equals
+from maas.client.bones.testing.server import ApplicationBuilder
+from maas.client.utils.testing import make_Credentials
+from maas.client.viscera import Origin
+from testtools.matchers import ContainsDict, Equals, IsInstance, MatchesStructure
from .. import machines
-from ...testing import (
- make_name_without_spaces,
- TestCase,
-)
from ..testing import bind
+from ...bones import CallError
+from ...bones.testing import api_descriptions
+from ...enum import NodeStatus, PowerState, PowerStopMode
+from ...errors import OperationNotAllowed
+from ...testing import make_name_without_spaces, TestCase
+from ..pods import Pod, Pods
-def make_origin():
- # Create a new origin with Machines and Machine. The former refers to the
- # latter via the origin, hence why it must be bound.
+def make_pods_origin():
+ """
+ Create a new origin with Pods and Pod. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(Pods, Pod)
+
+
+def make_machines_origin():
+ """
+ Create a new origin with Machines and Machine. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
return bind(machines.Machines, machines.Machine)
-class TestMachine(TestCase):
+def make_get_details_coroutine(system_id, return_value=""):
+ async def coroutine(system_id):
+ return return_value
+ return coroutine
+
+
+class TestMachine(TestCase):
def test__string_representation_includes_only_system_id_and_hostname(self):
- machine = machines.Machine({
+ machine = machines.Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ self.assertThat(
+ repr(machine),
+ Equals(
+ ""
+ % machine._data
+ ),
+ )
+
+ def test__get_power_parameters(self):
+ machine = make_machines_origin().Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ machine._handler.power_parameters.return_value = power_parameters
+ self.assertThat(machine.get_power_parameters(), Equals(power_parameters))
+ machine._handler.power_parameters.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__set_power(self):
+ orig_power_type = make_name_without_spaces("power_type")
+ new_power_type = make_name_without_spaces("power_type")
+ machine = make_machines_origin().Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "power_type": orig_power_type,
+ }
+ )
+ power_parameters = {"key": make_name_without_spaces("value")}
+ machine._handler.update.return_value = {"power_type": new_power_type}
+ machine.set_power(new_power_type, power_parameters)
+ machine._handler.update.assert_called_once_with(
+ system_id=machine.system_id,
+ power_type=new_power_type,
+ power_parameters=power_parameters,
+ )
+ self.assertThat(machine.power_type, Equals(new_power_type))
+
+ def test__abort(self):
+ data = {
"system_id": make_name_without_spaces("system-id"),
"hostname": make_name_without_spaces("hostname"),
- })
- self.assertThat(repr(machine), Equals(
- ""
- % machine._data))
-
- def test__deploy(self):
- Machine = make_origin().Machine
- Machine._handler.deploy.return_value = {}
- machine = Machine({
+ }
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.abort.return_value = data
+ comment = make_name_without_spaces("comment")
+ self.assertThat(machine.abort(comment=comment), Equals(origin.Machine(data)))
+ machine._handler.abort.assert_called_once_with(
+ system_id=machine.system_id, comment=comment
+ )
+
+ def test__clear_default_gateways(self):
+ data = {
"system_id": make_name_without_spaces("system-id"),
"hostname": make_name_without_spaces("hostname"),
- })
- machine.deploy(
- distro_series='ubuntu/xenial',
- hwe_kernel='hwe-x',
+ }
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.clear_default_gateways.return_value = data
+ self.assertThat(machine.clear_default_gateways(), Equals(origin.Machine(data)))
+ machine._handler.clear_default_gateways.assert_called_once_with(
+ system_id=machine.system_id
)
+
+ def test__commissioning_without_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.COMMISSIONING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.commission.return_value = data
+ machine.commission()
+ self.assertThat(machine.status, Equals(NodeStatus.COMMISSIONING))
+ machine._handler.commission.assert_called_once_with(system_id=machine.system_id)
+
+ def test__commissioning_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.COMMISSIONING,
+ }
+ ready_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.READY,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.commission.return_value = data
+ machine._handler.read.return_value = ready_data
+ machine.commission(wait=True, wait_interval=0.1)
+ self.assertThat(machine.status, Equals(NodeStatus.READY))
+ machine._handler.commission.assert_called_once_with(system_id=machine.system_id)
+
+ def test__commissioning_and_testing_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.COMMISSIONING,
+ }
+ testing_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.TESTING,
+ }
+ ready_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.READY,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.commission.return_value = data
+ machine._handler.read.side_effect = [testing_data, ready_data]
+ machine.commission(wait=True, wait_interval=0.1)
+ self.assertThat(machine.status, Equals(NodeStatus.READY))
+ machine._handler.commission.assert_called_once_with(system_id=machine.system_id)
+
+ def test__commission_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.COMMISSIONING,
+ }
+ failed_commissioning_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_COMMISSIONING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.commission.return_value = data
+ machine._handler.read.return_value = failed_commissioning_data
+ self.assertRaises(
+ machines.FailedCommissioning,
+ machine.commission,
+ wait=True,
+ wait_interval=0.1,
+ )
+
+ def test__commission_with_no_tests(self):
+ # 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 = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.COMMISSIONING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.commission.return_value = data
+ machine.commission(testing_scripts=random.choice(["", [], "none"]))
+ self.assertThat(machine.status, Equals(NodeStatus.COMMISSIONING))
+ machine._handler.commission.assert_called_once_with(
+ system_id=machine.system_id, testing_scripts=["none"]
+ )
+
+ def test__deploy_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.DEPLOYING,
+ }
+ deployed_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.DEPLOYED,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.deploy.return_value = data
+ machine._handler.read.return_value = deployed_data
+ machine.deploy(wait=True, wait_interval=0.1)
+ self.assertThat(machine.status, Equals(NodeStatus.DEPLOYED))
+ machine._handler.deploy.assert_called_once_with(system_id=machine.system_id)
+
+ def test__deploy_with_kvm_install(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(install_kvm=True, wait=False)
machine._handler.deploy.assert_called_once_with(
+ 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")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.DEPLOYING,
+ }
+ failed_deploy_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_DEPLOYMENT,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.deploy.return_value = data
+ machine._handler.read.return_value = failed_deploy_data
+ self.assertRaises(
+ machines.FailedDeployment, machine.deploy, wait=True, wait_interval=0.1
+ )
+
+ def test__enter_rescue_mode(self):
+ rescue_mode_machine = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "status": NodeStatus.ENTERING_RESCUE_MODE,
+ }
+ machine = make_machines_origin().Machine(rescue_mode_machine)
+ machine._handler.rescue_mode.return_value = rescue_mode_machine
+ self.assertThat(machine.enter_rescue_mode(), Equals(machine))
+ machine._handler.rescue_mode.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__enter_rescue_mode_operation_not_allowed(self):
+ machine = make_machines_origin().Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ # Mock the call to content.decode in the CallError constructor
+ content = Mock()
+ content.decode = Mock(return_value="")
+ machine._handler.rescue_mode.side_effect = CallError(
+ request={"method": "GET", "uri": "www.example.com"},
+ response=Mock(status=HTTPStatus.FORBIDDEN),
+ content=content,
+ call="",
+ )
+ self.assertRaises(OperationNotAllowed, machine.enter_rescue_mode)
+
+ def test__enter_rescue_mode_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ rm_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.ENTERING_RESCUE_MODE,
+ }
+ erm_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.RESCUE_MODE,
+ }
+ rm_machine = make_machines_origin().Machine(rm_data)
+ erm_machine = make_machines_origin().Machine(erm_data)
+ rm_machine._handler.rescue_mode.return_value = rm_data
+ rm_machine._handler.read.return_value = erm_data
+ result = rm_machine.enter_rescue_mode(wait=True, wait_interval=0.1)
+ self.assertThat(result.status, Equals(erm_machine.status))
+ rm_machine._handler.rescue_mode.assert_called_once_with(
+ system_id=rm_machine.system_id
+ )
+
+ def test__enter_rescue_mode_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.ENTERING_RESCUE_MODE,
+ }
+ failed_enter_rescue_mode_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_ENTERING_RESCUE_MODE,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.rescue_mode.return_value = data
+ machine._handler.read.return_value = failed_enter_rescue_mode_data
+ self.assertRaises(
+ machines.RescueModeFailure,
+ machine.enter_rescue_mode,
+ wait=True,
+ wait_interval=0.1,
+ )
+
+ def test__exit_rescue_mode(self):
+ exit_machine = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "status": NodeStatus.EXITING_RESCUE_MODE,
+ }
+ machine = make_machines_origin().Machine(exit_machine)
+ machine._handler.exit_rescue_mode.return_value = exit_machine
+ self.assertThat(machine.exit_rescue_mode(), Equals(machine))
+ machine._handler.exit_rescue_mode.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__exit_rescue_mode_operation_not_allowed(self):
+ machine = make_machines_origin().Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ # Mock the call to content.decode in the CallError constructor
+ content = Mock()
+ content.decode = Mock(return_value="")
+ machine._handler.exit_rescue_mode.side_effect = CallError(
+ request={"method": "GET", "uri": "www.example.com"},
+ response=Mock(status=HTTPStatus.FORBIDDEN),
+ content=content,
+ call="",
+ )
+ self.assertRaises(OperationNotAllowed, machine.exit_rescue_mode)
+
+ def test__exit_rescue_mode_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.EXITING_RESCUE_MODE,
+ }
+ deployed_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.DEPLOYED,
+ }
+ machine = make_machines_origin().Machine(data)
+ deployed_machine = make_machines_origin().Machine(deployed_data)
+ machine._handler.exit_rescue_mode.return_value = data
+ machine._handler.read.return_value = deployed_data
+ result = machine.exit_rescue_mode(wait=True, wait_interval=0.1)
+ self.assertThat(result.status, Equals(deployed_machine.status))
+ machine._handler.exit_rescue_mode.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__exit_rescue_mode_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.EXITING_RESCUE_MODE,
+ }
+ failed_exit_rescue_mode_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_EXITING_RESCUE_MODE,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.exit_rescue_mode.return_value = data
+ machine._handler.read.return_value = failed_exit_rescue_mode_data
+ self.assertRaises(
+ machines.RescueModeFailure,
+ machine.exit_rescue_mode,
+ wait=True,
+ wait_interval=0.1,
+ )
+
+ def test__get_curtin_config(self):
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ config = make_name_without_spaces("config")
+ machine._handler.get_curtin_config.return_value = config
+ self.assertThat(machine.get_curtin_config(), Equals(config))
+ machine._handler.get_curtin_config.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__mark_broken(self):
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.mark_broken.return_value = data
+ comment = make_name_without_spaces("comment")
+ self.assertThat(
+ machine.mark_broken(comment=comment), Equals(origin.Machine(data))
+ )
+ machine._handler.mark_broken.assert_called_once_with(
+ system_id=machine.system_id, comment=comment
+ )
+
+ def test__mark_fixed(self):
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.mark_fixed.return_value = data
+ comment = make_name_without_spaces("comment")
+ self.assertThat(
+ machine.mark_fixed(comment=comment), Equals(origin.Machine(data))
+ )
+ machine._handler.mark_fixed.assert_called_once_with(
+ system_id=machine.system_id, comment=comment
+ )
+
+ def test__release_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.RELEASING,
+ }
+ allocated_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.ALLOCATED,
+ }
+ machine = make_machines_origin().Machine(data)
+ allocated_machine = make_machines_origin().Machine(allocated_data)
+ machine._handler.release.return_value = data
+ machine._handler.read.return_value = allocated_data
+ result = machine.release(wait=True, wait_interval=0.1)
+ self.assertThat(result.status, Equals(allocated_machine.status))
+ machine._handler.release.assert_called_once_with(system_id=machine.system_id)
+
+ def test__release_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.RELEASING,
+ }
+ failed_release_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_RELEASING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.release.return_value = data
+ machine._handler.read.return_value = failed_release_data
+ self.assertRaises(
+ machines.FailedReleasing, machine.release, wait=True, wait_interval=0.1
+ )
+
+ def test__release_with_wait_failed_disk_erasing(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.RELEASING,
+ }
+ failed_disk_erase_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.FAILED_DISK_ERASING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.release.return_value = data
+ machine._handler.read.return_value = failed_disk_erase_data
+ self.assertRaises(
+ machines.FailedDiskErasing, machine.release, wait=True, wait_interval=0.1
+ )
+
+ def test__release_with_deleted_machine(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "status": NodeStatus.RELEASING,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.release.return_value = data
+ machine._handler.read.side_effect = CallError(
+ request={"method": "GET", "uri": "www.example.com"},
+ response=Mock(status=HTTPStatus.NOT_FOUND),
+ content=b"",
+ call="",
+ )
+ result = machine.release(wait=True, wait_interval=0.1)
+ self.assertThat(result.status, Equals(NodeStatus.RELEASING))
+ machine._handler.release.assert_called_once_with(system_id=machine.system_id)
+
+ def test__power_on_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.OFF,
+ }
+ power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ON,
+ }
+ machine = make_machines_origin().Machine(data)
+ powered_machine = make_machines_origin().Machine(power_data)
+ machine._handler.power_on.return_value = data
+ machine._handler.read.return_value = power_data
+ result = machine.power_on(wait=True, wait_interval=0.1)
+ self.assertThat(result.power_state, Equals(powered_machine.power_state))
+ machine._handler.power_on.assert_called_once_with(system_id=machine.system_id)
+
+ def test__power_on_doesnt_wait_for_unknown(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.UNKNOWN,
+ }
+ power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.UNKNOWN,
+ }
+ machine = make_machines_origin().Machine(data)
+ powered_machine = make_machines_origin().Machine(power_data)
+ machine._handler.power_on.return_value = data
+ machine._handler.read.return_value = power_data
+ result = machine.power_on(wait=True, wait_interval=0.1)
+ self.assertThat(result.power_state, Equals(powered_machine.power_state))
+ assert machine._handler.read.call_count == 0
+
+ def test__power_on_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.OFF,
+ }
+ failed_power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ERROR,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.power_on.return_value = data
+ machine._handler.read.return_value = failed_power_data
+ self.assertRaises(
+ machines.PowerError, machine.power_on, wait=True, wait_interval=0.1
+ )
+
+ def test__power_off_with_wait(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ON,
+ }
+ power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.OFF,
+ }
+ machine = make_machines_origin().Machine(data)
+ powered_machine = make_machines_origin().Machine(power_data)
+ machine._handler.power_off.return_value = data
+ machine._handler.read.return_value = power_data
+ result = machine.power_off(wait=True, wait_interval=0.1)
+ self.assertThat(result.power_state, Equals(powered_machine.power_state))
+ machine._handler.power_off.assert_called_once_with(
+ system_id=machine.system_id, stop_mode=PowerStopMode.HARD.value
+ )
+
+ def test__power_off_soft_mode(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ON,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.power_off.return_value = data
+ machine.power_off(stop_mode=PowerStopMode.SOFT, wait=False)
+ machine._handler.power_off.assert_called_once_with(
+ system_id=machine.system_id, stop_mode=PowerStopMode.SOFT.value
+ )
+
+ def test__power_off_doesnt_wait_for_unknown(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.UNKNOWN,
+ }
+ power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.UNKNOWN,
+ }
+ machine = make_machines_origin().Machine(data)
+ powered_machine = make_machines_origin().Machine(power_data)
+ machine._handler.power_off.return_value = data
+ machine._handler.read.return_value = power_data
+ result = machine.power_off(wait=True, wait_interval=0.1)
+ self.assertThat(result.power_state, Equals(powered_machine.power_state))
+ self.assertThat(machine._handler.read.call_count, Equals(0))
+
+ def test__power_off_with_wait_failed(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ON,
+ }
+ failed_power_data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.ERROR,
+ }
+ machine = make_machines_origin().Machine(data)
+ machine._handler.power_off.return_value = data
+ machine._handler.read.return_value = failed_power_data
+ self.assertRaises(
+ machines.PowerError, machine.power_off, wait=True, wait_interval=0.1
+ )
+
+ def test__query_power_state(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "power_state": PowerState.OFF,
+ }
+ query_data = {"state": "on"}
+ machine = make_machines_origin().Machine(data)
+ machine._handler.query_power_state.return_value = query_data
+ result = machine.query_power_state()
+ self.assertIsInstance(result, PowerState)
+ self.assertEquals(PowerState.ON, result)
+ self.assertEqual(PowerState.ON, machine.power_state)
+
+ def test__get_details(self):
+ return_val = (
+ b"S\x01\x00\x00\x05lshw\x00\xf2\x00\x00\x00\x00\n\n\n\n\n\n
\n\x05lldp\x00F\x00"
+ b'\x00\x00\x00\n'
+ b' \n\x00'
+ )
+ machine = make_machines_origin().Machine(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ machine._handler.details = make_get_details_coroutine(
+ machine.system_id, return_value=return_val
+ )
+ data = machine.get_details()
+ self.assertItemsEqual(["lldp", "lshw"], data.keys())
+ lldp = ElementTree.fromstring(data["lldp"])
+ lshw = ElementTree.fromstring(data["lshw"])
+ assert IsInstance(lldp, ElementTree)
+ assert IsInstance(lshw, ElementTree)
+
+ def test__restore_default_configuration(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {"system_id": system_id, "hostname": hostname}
+ machine = make_machines_origin().Machine(data)
+ machine._handler.restore_default_configuration.return_value = data
+ machine.restore_default_configuration()
+ self.assertEqual(data, machine._data)
+ machine._handler.restore_default_configuration.assert_called_once_with(
+ system_id=machine.system_id
+ )
+
+ def test__restore_networking_configuration(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {"system_id": system_id, "hostname": hostname}
+ machine = make_machines_origin().Machine(data)
+ mock_restore_networking = machine._handler.restore_networking_configuration
+ mock_restore_networking.return_value = data
+ machine.restore_networking_configuration()
+ self.assertEqual(data, machine._data)
+ mock_restore_networking.assert_called_once_with(system_id=machine.system_id)
+
+ def test__restore_storage_configuration(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {"system_id": system_id, "hostname": hostname}
+ machine = make_machines_origin().Machine(data)
+ mock_restore_storage = machine._handler.restore_storage_configuration
+ mock_restore_storage.return_value = data
+ machine.restore_storage_configuration()
+ self.assertEqual(data, machine._data)
+ mock_restore_storage.assert_called_once_with(system_id=machine.system_id)
+
+ def test__save_updates_owner_data(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "owner_data": {"hello": "world", "delete": "me", "keep": "me"},
+ }
+ machine = make_machines_origin().Machine(data)
+ del machine.owner_data["delete"]
+ machine.owner_data["hello"] = "whole new world"
+ machine.owner_data["new"] = "brand-new"
+ machine._handler.set_owner_data.return_value = {}
+ machine.save()
+ self.assertThat(machine._handler.update.call_count, Equals(0))
+ self.assertThat(
+ machine.owner_data,
+ Equals({"hello": "whole new world", "new": "brand-new", "keep": "me"}),
+ )
+ machine._handler.set_owner_data.assert_called_once_with(
+ system_id=machine.system_id,
+ delete="",
+ hello="whole new world",
+ new="brand-new",
+ )
+
+ def test__save_updates_owner_data_with_replace(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {
+ "system_id": system_id,
+ "hostname": hostname,
+ "owner_data": {"hello": "world", "delete": "me", "keep": "me"},
+ }
+ machine = make_machines_origin().Machine(data)
+ machine.owner_data = {
+ "hello": "whole new world",
+ "keep": "me",
+ "new": "brand-new",
+ }
+ machine._handler.set_owner_data.return_value = {}
+ machine.save()
+ self.assertThat(machine._handler.update.call_count, Equals(0))
+ self.assertThat(
+ machine.owner_data,
+ Equals({"hello": "whole new world", "new": "brand-new", "keep": "me"}),
+ )
+ machine._handler.set_owner_data.assert_called_once_with(
system_id=machine.system_id,
- distro_series='ubuntu/xenial',
- hwe_kernel='hwe-x',
+ delete="",
+ hello="whole new world",
+ new="brand-new",
+ )
+
+ def test__lock(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {"system_id": system_id, "hostname": hostname, "locked": False}
+ new_data = {"system_id": system_id, "hostname": hostname, "locked": True}
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.lock.return_value = new_data
+ comment = make_name_without_spaces("comment")
+ self.assertThat(machine.lock(comment=comment), Equals(origin.Machine(new_data)))
+ machine._handler.lock.assert_called_once_with(
+ system_id=machine.system_id, comment=comment
)
+ self.assertThat(machine.locked, Equals(new_data["locked"]))
+
+ def test__unlock(self):
+ system_id = make_name_without_spaces("system-id")
+ hostname = make_name_without_spaces("hostname")
+ data = {"system_id": system_id, "hostname": hostname, "locked": True}
+ new_data = {"system_id": system_id, "hostname": hostname, "locked": False}
+ origin = make_machines_origin()
+ machine = origin.Machine(data)
+ machine._handler.unlock.return_value = new_data
+ comment = make_name_without_spaces("comment")
+ self.assertThat(
+ machine.unlock(comment=comment), Equals(origin.Machine(new_data))
+ )
+ machine._handler.unlock.assert_called_once_with(
+ system_id=machine.system_id, comment=comment
+ )
+ self.assertThat(machine.locked, Equals(new_data["locked"]))
+
+
+class TestMachine_APIVersion(TestCase):
+
+ scenarios = tuple(
+ (name, dict(version=version, description=description))
+ for name, version, description in api_descriptions
+ )
+
+ async def test__deploy(self):
+ builder = ApplicationBuilder(self.description)
+
+ @builder.handle("auth:Machine.deploy")
+ async def deploy(request, system_id):
+ self.assertThat(
+ request.params,
+ ContainsDict(
+ {
+ "distro_series": Equals("ubuntu/xenial"),
+ "hwe_kernel": Equals("hwe-x"),
+ }
+ ),
+ )
+ return {"system_id": system_id, "status_name": "Deploying"}
+
+ async with builder.serve() as baseurl:
+ origin = await Origin.fromURL(baseurl, credentials=make_Credentials())
+ machine = origin.Machine(
+ {"system_id": make_name_without_spaces("system-id")}
+ )
+ machine = await machine.deploy(
+ distro_series="ubuntu/xenial", hwe_kernel="hwe-x"
+ )
+ self.assertThat(
+ machine, MatchesStructure.byEquality(status_name="Deploying")
+ )
class TestMachines(TestCase):
+ def test__create(self):
+ origin = make_machines_origin()
+ Machines, Machine = origin.Machines, origin.Machine
+ Machines._handler.create.return_value = {}
+ observed = Machines.create(
+ "amd64",
+ ["00:11:22:33:44:55", "00:11:22:33:44:AA"],
+ "ipmi",
+ {"power_address": "localhost", "power_user": "root"},
+ subarchitecture="generic",
+ min_hwe_kernel="hwe-x",
+ hostname="new-machine",
+ domain="maas",
+ )
+ self.assertThat(observed, IsInstance(Machine))
+ Machines._handler.create.assert_called_once_with(
+ architecture="amd64",
+ mac_addresses=["00:11:22:33:44:55", "00:11:22:33:44:AA"],
+ power_type="ipmi",
+ power_parameters=(
+ '{"power_address": "localhost", ' '"power_user": "root"}'
+ ),
+ subarchitecture="generic",
+ min_hwe_kernel="hwe-x",
+ hostname="new-machine",
+ domain="maas",
+ )
def test__allocate(self):
- Machines = make_origin().Machines
+ Machines = make_machines_origin().Machines
+ Machines._handler.allocate.return_value = {}
+ hostname = make_name_without_spaces("hostname")
+ Machines.allocate(
+ hostname=hostname,
+ architectures=["amd64/generic"],
+ cpus=4,
+ memory=1024.0,
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ )
+ Machines._handler.allocate.assert_called_once_with(
+ name=hostname, # API parameter is actually name, not hostname
+ arch=["amd64/generic"],
+ cpu_count="4",
+ mem="1024.0",
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ )
+
+ def test__allocate_with_pod(self):
+ Pod = make_pods_origin().Pod
+ pod = Pod({"name": make_name_without_spaces("pod")})
+ Machines = make_machines_origin().Machines
+ Machines._handler.allocate.return_value = {}
+ hostname = make_name_without_spaces("hostname")
+ Machines.allocate(
+ hostname=hostname,
+ architectures=["amd64/generic"],
+ cpus=4,
+ memory=1024.0,
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ pod=pod.name,
+ )
+ Machines._handler.allocate.assert_called_once_with(
+ name=hostname, # API parameter is actually name, not hostname
+ arch=["amd64/generic"],
+ cpu_count="4",
+ mem="1024.0",
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ pod=pod.name,
+ )
+
+ def test__allocate_with_not_pod(self):
+ Pod = make_pods_origin().Pod
+ pod = Pod({"name": make_name_without_spaces("pod")})
+ Machines = make_machines_origin().Machines
Machines._handler.allocate.return_value = {}
hostname = make_name_without_spaces("hostname")
Machines.allocate(
hostname=hostname,
- architecture='amd64/generic',
+ architectures=["amd64/generic"],
cpus=4,
memory=1024.0,
- tags=['foo', 'bar', '-baz'],
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ not_pod=pod.name,
)
Machines._handler.allocate.assert_called_once_with(
name=hostname, # API parameter is actually name, not hostname
- architecture='amd64/generic',
- cpu_count='4',
- mem='1024.0',
- tags=['foo', 'bar'],
- not_tags=['baz'],
+ arch=["amd64/generic"],
+ cpu_count="4",
+ mem="1024.0",
+ tags=["foo", "bar"],
+ not_tags=["baz"],
+ not_pod=pod.name,
+ )
+
+ def test__get_power_parameters_for_with_empty_list(self):
+ Machines = make_machines_origin().Machines
+ self.assertThat(Machines.get_power_parameters_for(system_ids=[]), Equals({}))
+
+ def test__get_power_parameters_for_with_system_ids(self):
+ power_parameters = {
+ make_name_without_spaces("system_id"): {
+ "key": make_name_without_spaces("value")
+ }
+ for _ in range(3)
+ }
+ Machines = make_machines_origin().Machines
+ Machines._handler.power_parameters.return_value = power_parameters
+ self.assertThat(
+ Machines.get_power_parameters_for(system_ids=power_parameters.keys()),
+ Equals(power_parameters),
+ )
+ Machines._handler.power_parameters.assert_called_once_with(
+ id=power_parameters.keys()
)
diff --git a/maas/client/viscera/tests/test_nodes.py b/maas/client/viscera/tests/test_nodes.py
new file mode 100644
index 00000000..047f2d6b
--- /dev/null
+++ b/maas/client/viscera/tests/test_nodes.py
@@ -0,0 +1,503 @@
+"""Test for `maas.client.viscera.nodes`."""
+
+from copy import deepcopy
+
+from testtools.matchers import (
+ Equals,
+ IsInstance,
+ MatchesAll,
+ MatchesListwise,
+ MatchesStructure,
+)
+
+from .. import nodes
+from ..controllers import RackController, RegionController
+from ..tags import Tags, Tag
+from ..devices import Device
+from ..domains import Domain
+from ..machines import Machine
+from ..resource_pools import ResourcePool
+from ..testing import bind
+from ...enum import NodeType
+from ...testing import make_name_without_spaces, TestCase
+
+
+def make_origin():
+ # Create a new origin with Nodes and Node. The former refers to the
+ # latter via the origin, hence why it must be bound.
+ return bind(
+ nodes.Nodes,
+ nodes.Node,
+ Device,
+ Domain,
+ Machine,
+ RackController,
+ RegionController,
+ ResourcePool,
+ Tags,
+ Tag,
+ )
+
+
+class TestNode(TestCase):
+ def test__string_representation_includes_only_system_id_and_hostname(self):
+ node = nodes.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+ self.assertThat(
+ repr(node),
+ Equals("" % node._data),
+ )
+
+ def test__read(self):
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+
+ origin = make_origin()
+ origin.Node._handler.read.return_value = data
+
+ node_observed = origin.Node.read(data["system_id"])
+ node_expected = origin.Node(data)
+ self.assertThat(node_observed, Equals(node_expected))
+
+ def test__read_domain(self):
+ domain = {"name": make_name_without_spaces("domain")}
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "domain": domain,
+ }
+ origin = make_origin()
+ origin.Node._handler.read.return_value = data
+ node = origin.Node.read(data["system_id"])
+ domain = origin.Domain(domain)
+ self.assertThat(node.domain, Equals(domain))
+
+ def test__save_change_pool(self):
+ pool_data = {"id": 1, "name": "pool1", "description": "pool1"}
+ new_pool_data = {"id": 2, "name": "pool2", "description": "pool2"}
+ system_id = make_name_without_spaces("system-id")
+ node_data = {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "pool": pool_data,
+ }
+
+ origin = make_origin()
+ origin.ResourcePool._handler.read.return_value = new_pool_data
+ origin.Node._handler.params = ["system_id", "id"]
+ origin.Node._handler.read.return_value = node_data
+ origin.Node._handler.update.return_value = deepcopy(node_data)
+ origin.Node._handler.update.return_value["pool"] = new_pool_data
+
+ new_pool = origin.ResourcePool.read(2)
+ node = origin.Node.read(system_id)
+ node.pool = new_pool
+ node.save()
+ origin.Node._handler.update.assert_called_once_with(
+ id=1, pool="pool2", system_id=system_id
+ )
+
+ def test__save_change_domain(self):
+ domain_data = {"id": 1, "name": "domain1"}
+ new_domain_data = {"id": 2, "name": "domain2"}
+ system_id = make_name_without_spaces("system-id")
+ node_data = {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "domain": domain_data,
+ }
+
+ origin = make_origin()
+ origin.Domain._handler.read.return_value = new_domain_data
+ origin.Node._handler.params = ["system_id", "id"]
+ origin.Node._handler.read.return_value = node_data
+ origin.Node._handler.update.return_value = deepcopy(node_data)
+ origin.Node._handler.update.return_value["domain"] = new_domain_data
+
+ new_domain = origin.Domain.read(2)
+ node = origin.Node.read(system_id)
+ node.domain = new_domain
+ node.save()
+ origin.Node._handler.update.assert_called_once_with(
+ id=1, domain=2, system_id=system_id
+ )
+
+ def test__as_machine_requires_machine_type(self):
+ origin = make_origin()
+ device_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.DEVICE.value,
+ }
+ )
+ rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ }
+ )
+ region_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ }
+ )
+ region_rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ }
+ )
+ self.assertRaises(ValueError, device_node.as_machine)
+ self.assertRaises(ValueError, rack_node.as_machine)
+ self.assertRaises(ValueError, region_node.as_machine)
+ self.assertRaises(ValueError, region_rack_node.as_machine)
+
+ def test__as_machine_returns_machine_type(self):
+ origin = make_origin()
+ machine_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.MACHINE.value,
+ }
+ )
+ machine = machine_node.as_machine()
+ self.assertIsInstance(machine, Machine)
+
+ def test__as_device_requires_device_type(self):
+ origin = make_origin()
+ machine_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.MACHINE.value,
+ }
+ )
+ rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ }
+ )
+ region_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ }
+ )
+ region_rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ }
+ )
+ self.assertRaises(ValueError, machine_node.as_device)
+ self.assertRaises(ValueError, rack_node.as_device)
+ self.assertRaises(ValueError, region_node.as_device)
+ self.assertRaises(ValueError, region_rack_node.as_device)
+
+ def test__as_device_returns_device_type(self):
+ origin = make_origin()
+ device_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.DEVICE.value,
+ }
+ )
+ device = device_node.as_device()
+ self.assertIsInstance(device, Device)
+
+ def test__as_rack_controller_requires_rack_types(self):
+ origin = make_origin()
+ machine_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.MACHINE.value,
+ }
+ )
+ device_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.DEVICE.value,
+ }
+ )
+ region_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ }
+ )
+ self.assertRaises(ValueError, machine_node.as_rack_controller)
+ self.assertRaises(ValueError, device_node.as_rack_controller)
+ self.assertRaises(ValueError, region_node.as_rack_controller)
+
+ def test__as_rack_controller_returns_rack_type(self):
+ origin = make_origin()
+ rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ }
+ )
+ region_rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ }
+ )
+ rack = rack_node.as_rack_controller()
+ region_rack = region_rack_node.as_rack_controller()
+ self.assertIsInstance(rack, RackController)
+ self.assertIsInstance(region_rack, RackController)
+
+ def test__as_region_controller_requires_region_types(self):
+ origin = make_origin()
+ machine_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.MACHINE.value,
+ }
+ )
+ device_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.DEVICE.value,
+ }
+ )
+ rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.RACK_CONTROLLER.value,
+ }
+ )
+ self.assertRaises(ValueError, machine_node.as_region_controller)
+ self.assertRaises(ValueError, device_node.as_region_controller)
+ self.assertRaises(ValueError, rack_node.as_region_controller)
+
+ def test__as_region_controller_returns_region_type(self):
+ origin = make_origin()
+ region_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_CONTROLLER.value,
+ }
+ )
+ region_rack_node = origin.Node(
+ {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ "node_type": NodeType.REGION_AND_RACK_CONTROLLER.value,
+ }
+ )
+ region = region_node.as_region_controller()
+ region_rack = region_rack_node.as_region_controller()
+ self.assertIsInstance(region, RegionController)
+ self.assertIsInstance(region_rack, RegionController)
+
+ def test__delete(self):
+ Node = make_origin().Node
+
+ system_id = make_name_without_spaces("system-id")
+ node = Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ }
+ )
+
+ node.delete()
+ Node._handler.delete.assert_called_once_with(system_id=node.system_id)
+
+ def test__tags(self):
+ origin = make_origin()
+
+ tag_names = [make_name_without_spaces("tag") for _ in range(3)]
+
+ system_id = make_name_without_spaces("system-id")
+ node = origin.Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "tag_names": tag_names,
+ }
+ )
+
+ self.assertThat(
+ node.tags,
+ MatchesListwise(
+ [
+ MatchesAll(
+ IsInstance(origin.Tag), MatchesStructure(name=Equals(tag_name))
+ )
+ for tag_name in tag_names
+ ]
+ ),
+ )
+
+ def test__tags_add(self):
+ origin = make_origin()
+
+ tag_names = [make_name_without_spaces("tag") for _ in range(3)]
+
+ system_id = make_name_without_spaces("system-id")
+ node = origin.Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "tag_names": list(tag_names),
+ }
+ )
+
+ tag = origin.Tag({"name": make_name_without_spaces("newtag")})
+ node.tags.add(tag)
+
+ origin.Tag._handler.update_nodes.assert_called_once_with(
+ name=tag.name, add=node.system_id
+ )
+
+ self.assertThat(
+ node.tags,
+ MatchesListwise(
+ [
+ MatchesAll(
+ IsInstance(origin.Tag), MatchesStructure(name=Equals(tag_name))
+ )
+ for tag_name in tag_names + [tag.name]
+ ]
+ ),
+ )
+
+ def test__tags_add_requires_Tag(self):
+ origin = make_origin()
+
+ tag_names = [make_name_without_spaces("tag") for _ in range(3)]
+
+ system_id = make_name_without_spaces("system-id")
+ node = origin.Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "tag_names": list(tag_names),
+ }
+ )
+
+ tag = origin.Tag({"name": make_name_without_spaces("newtag")})
+ self.assertRaises(TypeError, node.tags.add, tag.name)
+
+ def test__tags_remove(self):
+ origin = make_origin()
+
+ tag_names = [make_name_without_spaces("tag") for _ in range(3)]
+
+ system_id = make_name_without_spaces("system-id")
+ node = origin.Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "tag_names": list(tag_names),
+ }
+ )
+
+ tag = origin.Tag({"name": tag_names[0]})
+ node.tags.remove(tag)
+
+ origin.Tag._handler.update_nodes.assert_called_once_with(
+ name=tag.name, remove=node.system_id
+ )
+
+ self.assertThat(
+ node.tags,
+ MatchesListwise(
+ [
+ MatchesAll(
+ IsInstance(origin.Tag), MatchesStructure(name=Equals(tag_name))
+ )
+ for tag_name in tag_names[1:]
+ ]
+ ),
+ )
+
+ def test__tags_remove_requires_Tag(self):
+ origin = make_origin()
+
+ tag_names = [make_name_without_spaces("tag") for _ in range(3)]
+
+ system_id = make_name_without_spaces("system-id")
+ node = origin.Node(
+ {
+ "id": 1,
+ "system_id": system_id,
+ "hostname": make_name_without_spaces("hostname"),
+ "tag_names": list(tag_names),
+ }
+ )
+
+ tag = origin.Tag({"name": tag_names[0]})
+ self.assertRaises(TypeError, node.tags.remove, tag.name)
+
+
+class TestNodes(TestCase):
+ def test__read(self):
+ data = {
+ "system_id": make_name_without_spaces("system-id"),
+ "hostname": make_name_without_spaces("hostname"),
+ }
+
+ origin = make_origin()
+ origin.Nodes._handler.read.return_value = [data]
+
+ nodes_observed = origin.Nodes.read()
+ nodes_expected = origin.Nodes([origin.Node(data)])
+ self.assertThat(nodes_observed, Equals(nodes_expected))
+
+ def test__read_with_hostnames(self):
+ origin = make_origin()
+ origin.Nodes._handler.read.return_value = []
+
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ origin.Nodes.read(hostnames=hostnames)
+ origin.Nodes._handler.read.assert_called_once_with(hostname=hostnames)
+
+ def test__read_with_normalized_hostnames(self):
+ origin = make_origin()
+ origin.Nodes._handler.read.return_value = []
+
+ hostnames = [make_name_without_spaces() for _ in range(3)]
+ origin.Nodes.read(
+ hostnames=[
+ "%s.%s" % (hostname, make_name_without_spaces())
+ for hostname in hostnames
+ ]
+ )
+ origin.Nodes._handler.read.assert_called_once_with(hostname=hostnames)
diff --git a/maas/client/viscera/tests/test_pods.py b/maas/client/viscera/tests/test_pods.py
new file mode 100644
index 00000000..45874665
--- /dev/null
+++ b/maas/client/viscera/tests/test_pods.py
@@ -0,0 +1,241 @@
+"""Test for `maas.client.viscera.pods`."""
+
+import random
+
+from testtools.matchers import Equals, IsInstance
+
+from ...errors import OperationNotAllowed
+from ..pods import Pod, Pods
+from ..testing import bind
+from ...testing import make_name_without_spaces, make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Pods and Pod. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(Pods, Pod)
+
+
+def make_pod():
+ """Returns a pod dictionary."""
+ return {
+ "id": random.randint(1, 100),
+ "type": make_name_without_spaces("type"),
+ "name": make_name_without_spaces("name"),
+ "architectures": [make_string_without_spaces() for _ in range(3)],
+ "capabilities": [make_string_without_spaces() for _ in range(3)],
+ "zone": {
+ "id": random.randint(1, 100),
+ "name": make_name_without_spaces("name"),
+ "description": make_name_without_spaces("description"),
+ },
+ "tags": [make_string_without_spaces() for _ in range(3)],
+ "cpu_over_commit_ratio": random.uniform(0, 10),
+ "memory_over_commit_ratio": random.uniform(0, 10),
+ "available": {
+ "cores": random.randint(1, 100),
+ "memory": random.randint(4096, 8192),
+ "local_storage": random.randint(1024, 1024 * 1024),
+ },
+ "used": {
+ "cores": random.randint(1, 100),
+ "memory": random.randint(4096, 8192),
+ "local_storage": random.randint(1024, 1024 * 1024),
+ },
+ "total": {
+ "cores": random.randint(1, 100),
+ "memory": random.randint(4096, 8192),
+ "local_storage": random.randint(1024, 1024 * 1024),
+ },
+ }
+
+
+class TestPods(TestCase):
+ def test__pods_create(self):
+ type = make_string_without_spaces()
+ power_address = make_string_without_spaces()
+ power_user = make_string_without_spaces()
+ power_pass = make_string_without_spaces()
+ name = make_string_without_spaces()
+ zone = make_string_without_spaces()
+ tags = make_string_without_spaces()
+ origin = make_origin()
+ Pods, Pod = origin.Pods, origin.Pod
+ Pods._handler.create.return_value = {}
+ observed = Pods.create(
+ type=type,
+ power_address=power_address,
+ power_user=power_user,
+ power_pass=power_pass,
+ name=name,
+ zone=zone,
+ tags=tags,
+ )
+ self.assertThat(observed, IsInstance(Pod))
+ Pods._handler.create.assert_called_once_with(
+ type=type,
+ power_address=power_address,
+ power_user=power_user,
+ power_pass=power_pass,
+ name=name,
+ zone=zone,
+ tags=tags,
+ )
+
+ def test__pods_create_raises_error_for_rsd_and_no_power_user(self):
+ origin = make_origin()
+ origin.Pods._handler.create.return_value = {}
+ self.assertRaises(
+ OperationNotAllowed,
+ origin.Pods.create,
+ type="rsd",
+ power_address=make_string_without_spaces(),
+ )
+
+ def test__pods_create_raises_error_for_rsd_and_no_power_pass(self):
+ origin = make_origin()
+ origin.Pods._handler.create.return_value = {}
+ self.assertRaises(
+ OperationNotAllowed,
+ origin.Pods.create,
+ type="rsd",
+ power_address=make_string_without_spaces(),
+ power_user=make_string_without_spaces(),
+ )
+
+ def test__pods_create_raises_type_error_for_zone(self):
+ origin = make_origin()
+ origin.Pods._handler.create.return_value = {}
+ self.assertRaises(
+ TypeError,
+ origin.Pods.create,
+ type=make_string_without_spaces(),
+ power_address=make_string_without_spaces(),
+ power_user=make_string_without_spaces(),
+ power_pass=make_string_without_spaces(),
+ zone=0.1,
+ )
+
+ def test__pods_read(self):
+ Pods = make_origin().Pods
+ pods = [make_pod() for _ in range(3)]
+ Pods._handler.read.return_value = pods
+ pods = Pods.read()
+ self.assertThat(len(pods), Equals(3))
+
+
+class TestPod(TestCase):
+ def test__pod_read(self):
+ Pod = make_origin().Pod
+ pod = make_pod()
+ Pod._handler.read.return_value = pod
+ self.assertThat(Pod.read(id=pod["id"]), Equals(Pod(pod)))
+ Pod._handler.read.assert_called_once_with(id=pod["id"])
+
+ def test__pod_refresh(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ pod.refresh()
+ Pod._handler.refresh.assert_called_once_with(id=pod_data["id"])
+
+ def test__pod_parameters(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ pod.parameters()
+ Pod._handler.parameters.assert_called_once_with(id=pod_data["id"])
+
+ def test__pod_compose(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ cores = random.randint(1, 100)
+ memory = random.randint(4096, 8192)
+ cpu_speed = random.randint(16, 256)
+ architecture = make_name_without_spaces("architecture")
+ storage = make_string_without_spaces()
+ hostname = make_name_without_spaces("hostname")
+ domain = random.randint(1, 10)
+ zone = random.randint(1, 10)
+ interfaces = make_string_without_spaces()
+ pod.compose(
+ cores=cores,
+ memory=memory,
+ cpu_speed=cpu_speed,
+ architecture=architecture,
+ storage=storage,
+ hostname=hostname,
+ domain=domain,
+ zone=zone,
+ interfaces=interfaces,
+ )
+ Pod._handler.compose.assert_called_once_with(
+ id=pod_data["id"],
+ cores=str(cores),
+ memory=str(memory),
+ cpu_speed=str(cpu_speed),
+ architecture=architecture,
+ storage=storage,
+ hostname=hostname,
+ domain=str(domain),
+ zone=str(zone),
+ interfaces=interfaces,
+ )
+
+ def test__pod_compose_raises_type_error_for_zone(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ cores = random.randint(1, 100)
+ memory = random.randint(4096, 8192)
+ cpu_speed = random.randint(16, 256)
+ architecture = make_name_without_spaces("architecture")
+ storage = make_string_without_spaces()
+ hostname = make_name_without_spaces("hostname")
+ domain = random.randint(1, 10)
+ zone = 0.1
+ interfaces = make_string_without_spaces()
+ self.assertRaises(
+ TypeError,
+ pod.compose,
+ cores=cores,
+ memory=memory,
+ cpu_speed=cpu_speed,
+ architecture=architecture,
+ storage=storage,
+ hostname=hostname,
+ domain=domain,
+ zone=zone,
+ interfaces=interfaces,
+ )
+
+ def test__pod_delete(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ pod.delete()
+ Pod._handler.delete.assert_called_once_with(id=pod_data["id"])
+
+ def test__save_add_tag(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ pod = Pod(pod_data)
+ tag = make_string_without_spaces()
+ pod.tags.append(tag)
+ Pod._handler.add_tag.return_value = None
+ pod.save()
+ Pod._handler.add_tag.assert_called_once_with(id=pod.id, tag=tag)
+
+ def test__save_remove_tag(self):
+ Pod = make_origin().Pod
+ pod_data = make_pod()
+ tag = make_string_without_spaces()
+ pod_data["tags"] = [tag]
+ pod = Pod(pod_data)
+ pod.tags.remove(tag)
+ Pod._handler.remove_tag.return_value = None
+ pod.save()
+ Pod._handler.remove_tag.assert_called_once_with(id=pod.id, tag=tag)
diff --git a/maas/client/viscera/tests/test_resource_pools.py b/maas/client/viscera/tests/test_resource_pools.py
new file mode 100644
index 00000000..a876df33
--- /dev/null
+++ b/maas/client/viscera/tests/test_resource_pools.py
@@ -0,0 +1,100 @@
+"""Tests for `maas.client.viscera.resource_pools`."""
+
+import random
+
+from testtools.matchers import Equals, IsInstance, MatchesStructure
+
+from .. import resource_pools
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with ResourcePool and ResourcePools. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(resource_pools.ResourcePools, resource_pools.ResourcePool)
+
+
+class TestResourcePools(TestCase):
+ def test_resource_pools_create(self):
+ origin = make_origin()
+ pool_id = random.randint(0, 100)
+ name = make_string_without_spaces()
+ description = make_string_without_spaces()
+ origin.ResourcePools._handler.create.return_value = {
+ "id": pool_id,
+ "name": name,
+ "description": description,
+ }
+ pool = origin.ResourcePools.create(name=name, description=description)
+ origin.ResourcePools._handler.create.assert_called_once_with(
+ name=name, description=description
+ )
+ self.assertThat(pool, IsInstance(origin.ResourcePool))
+ self.assertThat(
+ pool,
+ MatchesStructure.byEquality(id=pool_id, name=name, description=description),
+ )
+
+ def test_resource_pools_create_without_description(self):
+ origin = make_origin()
+ pool_id = random.randint(0, 100)
+ name = make_string_without_spaces()
+ description = ""
+ origin.ResourcePools._handler.create.return_value = {
+ "id": pool_id,
+ "name": name,
+ "description": description,
+ }
+ pool = origin.ResourcePools.create(name=name, description=description)
+ origin.ResourcePools._handler.create.assert_called_once_with(
+ name=name, description=description
+ )
+ self.assertThat(pool, IsInstance(origin.ResourcePool))
+ self.assertThat(
+ pool,
+ MatchesStructure.byEquality(id=pool_id, name=name, description=description),
+ )
+
+ def test_resource_pools_read(self):
+ ResourcePools = make_origin().ResourcePools
+ pools = [
+ {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "description": make_string_without_spaces(),
+ }
+ for _ in range(3)
+ ]
+ ResourcePools._handler.read.return_value = pools
+ pools = ResourcePools.read()
+ self.assertThat(len(pools), Equals(3))
+
+
+class TestResourcePool(TestCase):
+ def test_resource_pool_read(self):
+ ResourcePool = make_origin().ResourcePool
+ pool = {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "description": make_string_without_spaces(),
+ }
+ ResourcePool._handler.read.return_value = pool
+ self.assertThat(ResourcePool.read(id=pool["id"]), Equals(ResourcePool(pool)))
+ ResourcePool._handler.read.assert_called_once_with(id=pool["id"])
+
+ def test_resource_pool_delete(self):
+ ResourcePool = make_origin().ResourcePool
+ pool_id = random.randint(0, 100)
+ pool = ResourcePool(
+ {
+ "id": pool_id,
+ "name": make_string_without_spaces(),
+ "description": make_string_without_spaces(),
+ }
+ )
+ pool.delete()
+ ResourcePool._handler.delete.assert_called_once_with(id=pool_id)
diff --git a/maas/client/viscera/tests/test_spaces.py b/maas/client/viscera/tests/test_spaces.py
new file mode 100644
index 00000000..8781d0f2
--- /dev/null
+++ b/maas/client/viscera/tests/test_spaces.py
@@ -0,0 +1,75 @@
+"""Test for `maas.client.viscera.spaces`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..spaces import DeleteDefaultSpace, Space, Spaces
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Spaces and Space. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(Spaces, Space)
+
+
+class TestSpaces(TestCase):
+ def test__spaces_create(self):
+ Spaces = make_origin().Spaces
+ name = make_string_without_spaces()
+ description = make_string_without_spaces()
+ Spaces._handler.create.return_value = {
+ "id": 1,
+ "name": name,
+ "description": description,
+ }
+ Spaces.create(name=name, description=description)
+ Spaces._handler.create.assert_called_once_with(
+ name=name, description=description
+ )
+
+ def test__spaces_read(self):
+ """Spaces.read() returns a list of Spaces."""
+ Spaces = make_origin().Spaces
+ spaces = [
+ {"id": random.randint(0, 100), "name": make_string_without_spaces()}
+ for _ in range(3)
+ ]
+ Spaces._handler.read.return_value = spaces
+ spaces = Spaces.read()
+ self.assertThat(len(spaces), Equals(3))
+
+
+class TestSpace(TestCase):
+ def test__space_get_default(self):
+ Space = make_origin().Space
+ Space._handler.read.return_value = {
+ "id": 0,
+ "name": make_string_without_spaces(),
+ }
+ Space.get_default()
+ Space._handler.read.assert_called_once_with(id=0)
+
+ def test__space_read(self):
+ Space = make_origin().Space
+ space = {"id": random.randint(0, 100), "name": make_string_without_spaces()}
+ Space._handler.read.return_value = space
+ self.assertThat(Space.read(id=space["id"]), Equals(Space(space)))
+ Space._handler.read.assert_called_once_with(id=space["id"])
+
+ def test__space_delete(self):
+ Space = make_origin().Space
+ space_id = random.randint(1, 100)
+ space = Space({"id": space_id, "name": make_string_without_spaces()})
+ space.delete()
+ Space._handler.delete.assert_called_once_with(id=space_id)
+
+ def test__space_delete_default(self):
+ Space = make_origin().Space
+ space = Space({"id": 0, "name": make_string_without_spaces()})
+ self.assertRaises(DeleteDefaultSpace, space.delete)
diff --git a/maas/client/viscera/tests/test_sshkeys.py b/maas/client/viscera/tests/test_sshkeys.py
new file mode 100644
index 00000000..db00fbab
--- /dev/null
+++ b/maas/client/viscera/tests/test_sshkeys.py
@@ -0,0 +1,60 @@
+"""Test for `maas.client.viscera.sshkeys`."""
+
+import random
+
+from .. import sshkeys
+
+from ...testing import make_string_without_spaces, TestCase
+
+from ..testing import bind
+
+from testtools.matchers import Equals
+
+
+def make_origin():
+ return bind(sshkeys.SSHKeys, sshkeys.SSHKey)
+
+
+class TestSSHKeys(TestCase):
+ def test__sshkeys_create(self):
+ """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": ""}
+ SSHKeys.create(key=key)
+ SSHKeys._handler.create.assert_called_once_with(key=key)
+
+ def test__sshkeys_read(self):
+ """SSHKeys.read() returns a list of SSH keys."""
+ SSHKeys = make_origin().SSHKeys
+ keys = [
+ {
+ "id": random.randint(0, 100),
+ "key": make_string_without_spaces(),
+ "keysource": "",
+ }
+ for _ in range(3)
+ ]
+ SSHKeys._handler.read.return_value = keys
+ ssh_keys = SSHKeys.read()
+ self.assertThat(len(ssh_keys), Equals(3))
+
+
+class TestSSHKey(TestCase):
+ def test__sshkey_read(self):
+ """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": ""}
+ 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)
diff --git a/maas/client/viscera/tests/test_static_routes.py b/maas/client/viscera/tests/test_static_routes.py
new file mode 100644
index 00000000..bc69f6fd
--- /dev/null
+++ b/maas/client/viscera/tests/test_static_routes.py
@@ -0,0 +1,89 @@
+"""Test for `maas.client.viscera.static_routes`."""
+
+import random
+
+from testtools.matchers import Equals
+
+from ..static_routes import StaticRoute, StaticRoutes
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with StaticRoutes and StaticRoute. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(StaticRoutes, StaticRoute)
+
+
+class TestStaticRoutes(TestCase):
+ def test__static_routes_create(self):
+ StaticRoutes = make_origin().StaticRoutes
+ destination = random.randint(0, 100)
+ source = random.randint(0, 100)
+ gateway_ip = make_string_without_spaces()
+ metric = make_string_without_spaces()
+ StaticRoutes._handler.create.return_value = {
+ "id": 1,
+ "destination": destination,
+ "source": source,
+ "gateway_ip": gateway_ip,
+ "metric": metric,
+ }
+ StaticRoutes.create(
+ destination=destination, source=source, gateway_ip=gateway_ip, metric=metric
+ )
+ StaticRoutes._handler.create.assert_called_once_with(
+ destination=destination, source=source, gateway_ip=gateway_ip, metric=metric
+ )
+
+ def test__static_routes_read(self):
+ """StaticRoutes.read() returns a list of StaticRoutes."""
+ StaticRoutes = make_origin().StaticRoutes
+ static_routes = [
+ {
+ "id": random.randint(0, 100),
+ "destination": random.randint(0, 100),
+ "source": random.randint(0, 100),
+ "gateway_ip": make_string_without_spaces(),
+ "metric": make_string_without_spaces(),
+ }
+ for _ in range(3)
+ ]
+ StaticRoutes._handler.read.return_value = static_routes
+ static_routes = StaticRoutes.read()
+ self.assertThat(len(static_routes), Equals(3))
+
+
+class TestStaticRoute(TestCase):
+ def test__static_route_read(self):
+ StaticRoute = make_origin().StaticRoute
+ static_route = {
+ "id": random.randint(0, 100),
+ "destination": random.randint(0, 100),
+ "source": random.randint(0, 100),
+ "gateway_ip": make_string_without_spaces(),
+ "metric": make_string_without_spaces(),
+ }
+ StaticRoute._handler.read.return_value = static_route
+ self.assertThat(
+ StaticRoute.read(id=static_route["id"]), Equals(StaticRoute(static_route))
+ )
+ StaticRoute._handler.read.assert_called_once_with(id=static_route["id"])
+
+ def test__static_route_delete(self):
+ StaticRoute = make_origin().StaticRoute
+ static_route_id = random.randint(1, 100)
+ static_route = StaticRoute(
+ {
+ "id": static_route_id,
+ "destination": random.randint(0, 100),
+ "source": random.randint(0, 100),
+ "gateway_ip": make_string_without_spaces(),
+ "metric": make_string_without_spaces(),
+ }
+ )
+ static_route.delete()
+ StaticRoute._handler.delete.assert_called_once_with(id=static_route_id)
diff --git a/maas/client/viscera/tests/test_subnets.py b/maas/client/viscera/tests/test_subnets.py
new file mode 100644
index 00000000..1d56818c
--- /dev/null
+++ b/maas/client/viscera/tests/test_subnets.py
@@ -0,0 +1,116 @@
+"""Test for `maas.client.viscera.subnets`."""
+
+import random
+
+from testtools.matchers import Equals, IsInstance, MatchesAll, MatchesStructure
+
+from ..subnets import Subnet, Subnets
+
+from ..vlans import Vlan, Vlans
+
+from ..testing import bind
+from ...enum import RDNSMode
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Subnets, Subnet, Vlans, Vlan.
+ """
+ return bind(Subnets, Subnet, Vlans, Vlan)
+
+
+class TestSubnets(TestCase):
+ def test__subnets_create(self):
+ Subnets = make_origin().Subnets
+ cidr = make_string_without_spaces()
+ vlan = random.randint(5000, 8000)
+ name = make_string_without_spaces()
+ description = make_string_without_spaces()
+ Subnets._handler.create.return_value = {
+ "id": 1,
+ "cidr": cidr,
+ "vlan": vlan,
+ "name": name,
+ "description": description,
+ }
+ Subnets.create(cidr, vlan, name=name, description=description)
+ Subnets._handler.create.assert_called_once_with(
+ cidr=cidr, vlan=vlan, name=name, description=description
+ )
+
+ def test__subnets_read(self):
+ """Subnets.read() returns a list of Subnets."""
+ Subnets = make_origin().Subnets
+ subnets = [
+ {"id": random.randint(0, 100), "name": make_string_without_spaces()}
+ for _ in range(3)
+ ]
+ Subnets._handler.read.return_value = subnets
+ subnets = Subnets.read()
+ self.assertThat(len(subnets), Equals(3))
+
+
+class TestSubnet(TestCase):
+ def test__subnet_read(self):
+ Subnet = make_origin().Subnet
+ vlan_id = random.randint(5000, 8000)
+ subnet = {
+ "id": random.randint(0, 100),
+ "name": make_string_without_spaces(),
+ "vlan": {"id": vlan_id},
+ "rdns_mode": 2,
+ }
+ Subnet._handler.read.return_value = subnet
+ self.assertThat(Subnet.read(id=subnet["id"]), Equals(Subnet(subnet)))
+ self.assertThat(
+ Subnet(subnet).vlan,
+ MatchesAll(IsInstance(Vlan), MatchesStructure.byEquality(id=vlan_id)),
+ )
+ self.assertThat(Subnet(subnet).rdns_mode, Equals(RDNSMode.RFC2317))
+ Subnet._handler.read.assert_called_once_with(id=subnet["id"])
+
+ def test__subnet_update_vlan(self):
+ origin = make_origin()
+ Subnet, Vlan = origin.Subnet, origin.Vlan
+ Subnet._handler.params = ["id"]
+ subnet_id = random.randint(1, 100)
+ subnet = Subnet(
+ {
+ "id": subnet_id,
+ "name": make_string_without_spaces(),
+ "vlan": {"id": random.randint(1, 100)},
+ }
+ )
+ new_vlan = Vlan(
+ {"id": random.randint(101, 200), "fabric_id": random.randint(101, 200)}
+ )
+ subnet.vlan = new_vlan
+ Subnet._handler.update.return_value = {
+ "id": subnet.id,
+ "name": subnet.name,
+ "vlan": {"id": new_vlan.id},
+ }
+ subnet.save()
+ Subnet._handler.update.assert_called_once_with(id=subnet_id, vlan=new_vlan.id)
+
+ def test__subnet_doesnt_update_vlan_if_same(self):
+ Subnet = make_origin().Subnet
+ subnet_id = random.randint(1, 100)
+ subnet = Subnet(
+ {
+ "id": subnet_id,
+ "name": make_string_without_spaces(),
+ "vlan": {"id": random.randint(1, 100)},
+ }
+ )
+ subnet.vlan = subnet.vlan
+ subnet.save()
+ self.assertEqual(0, Subnet._handler.update.call_count)
+
+ def test__subnet_delete(self):
+ Subnet = make_origin().Subnet
+ subnet_id = random.randint(1, 100)
+ subnet = Subnet({"id": subnet_id, "name": make_string_without_spaces()})
+ subnet.delete()
+ Subnet._handler.delete.assert_called_once_with(id=subnet_id)
diff --git a/maas/client/viscera/tests/test_tags.py b/maas/client/viscera/tests/test_tags.py
new file mode 100644
index 00000000..fc48fef0
--- /dev/null
+++ b/maas/client/viscera/tests/test_tags.py
@@ -0,0 +1,71 @@
+"""Tests for `maas.client.viscera.tags`."""
+
+from testtools.matchers import Equals, IsInstance, MatchesStructure
+
+from .. import tags
+
+from ..testing import bind
+from ...testing import make_string_without_spaces, TestCase
+
+
+def make_origin():
+ """
+ Create a new origin with Tag and Tags. The former
+ refers to the latter via the origin, hence why it must be bound.
+ """
+ return bind(tags.Tags, tags.Tag)
+
+
+class TestTags(TestCase):
+ def test__tags_create(self):
+ origin = make_origin()
+ name = make_string_without_spaces()
+ comment = make_string_without_spaces()
+ origin.Tags._handler.create.return_value = {"name": name, "comment": comment}
+ tag = origin.Tags.create(name=name, comment=comment)
+ origin.Tags._handler.create.assert_called_once_with(name=name, comment=comment)
+ self.assertThat(tag, IsInstance(origin.Tag))
+ self.assertThat(tag, MatchesStructure.byEquality(name=name, comment=comment))
+
+ def test__tags_create_without_comment(self):
+ origin = make_origin()
+ name = make_string_without_spaces()
+ comment = ""
+ origin.Tags._handler.create.return_value = {"name": name, "comment": comment}
+ tag = origin.Tags.create(name=name)
+ origin.Tags._handler.create.assert_called_once_with(name=name)
+ self.assertThat(tag, IsInstance(origin.Tag))
+ self.assertThat(tag, MatchesStructure.byEquality(name=name, comment=comment))
+
+ def test__tags_read(self):
+ """Tags.read() returns a list of tags."""
+ Tags = make_origin().Tags
+ tags = [
+ {
+ "name": make_string_without_spaces(),
+ "comment": make_string_without_spaces(),
+ }
+ for _ in range(3)
+ ]
+ Tags._handler.read.return_value = tags
+ tags = Tags.read()
+ self.assertThat(len(tags), Equals(3))
+
+
+class TestTag(TestCase):
+ def test__tag_read(self):
+ Tag = make_origin().Tag
+ tag = {
+ "name": make_string_without_spaces(),
+ "comment": make_string_without_spaces(),
+ }
+ Tag._handler.read.return_value = tag
+ self.assertThat(Tag.read(name=tag["name"]), Equals(Tag(tag)))
+ Tag._handler.read.assert_called_once_with(name=tag["name"])
+
+ def test__tag_delete(self):
+ Tag = make_origin().Tag
+ tag_name = make_string_without_spaces()
+ tag = Tag({"name": tag_name, "comment": make_string_without_spaces()})
+ tag.delete()
+ Tag._handler.delete.assert_called_once_with(name=tag_name)
diff --git a/maas/client/viscera/tests/test_users.py b/maas/client/viscera/tests/test_users.py
index f3ca6f1b..114db1b7 100644
--- a/maas/client/viscera/tests/test_users.py
+++ b/maas/client/viscera/tests/test_users.py
@@ -1,18 +1,9 @@
"""Test for `maas.client.viscera.users`."""
-__all__ = []
-
-from testtools.matchers import (
- Equals,
- MatchesStructure,
-)
+from testtools.matchers import Equals, MatchesStructure
from .. import users
-from ...testing import (
- make_name_without_spaces,
- pick_bool,
- TestCase,
-)
+from ...testing import make_name_without_spaces, pick_bool, TestCase
from ..testing import bind
@@ -23,28 +14,30 @@ def make_origin():
class TestUser(TestCase):
-
def test__string_representation_includes_username_only(self):
- user = users.User({
- "username": make_name_without_spaces("username"),
- "email": make_name_without_spaces("user@"),
- "is_superuser": False,
- })
- self.assertThat(repr(user), Equals(
- "" % user._data))
+ user = users.User(
+ {
+ "username": make_name_without_spaces("username"),
+ "email": make_name_without_spaces("user@"),
+ "is_superuser": False,
+ }
+ )
+ self.assertThat(repr(user), Equals("" % user._data))
def test__string_representation_includes_username_only_for_admin(self):
- user = users.User({
- "username": make_name_without_spaces("username"),
- "email": make_name_without_spaces("user@"),
- "is_superuser": True,
- })
- self.assertThat(repr(user), Equals(
- "