diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ee8f1b9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: Lint + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Dependencies + shell: bash + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-lint.txt + - name: Lint + shell: bash + run: | + pylint nomad/ + black --check -l 120 -t py312 nomad/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..52f78cb --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,65 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python Nomad Test and Publish + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + registry_package: + types: [published] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + env: + NOMAD_IP: '127.0.0.1' + NOMAD_PORT: '4646' + + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.13'] # the oldest and newest support versions + nomad-version: ['1.4.14', '1.5.17', '1.6.10', '1.7.7', '1.8.4', '1.9.5'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Setup Nomad ${{ matrix.nomad-version }} + env: + NOMAD_VERSION: ${{ matrix.nomad-version }} + shell: bash + run: | + echo ${NOMAD_VERSION} + + echo "downloading nomad" + curl -L -o /tmp/nomad_${NOMAD_VERSION}_linux_amd64.zip https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_linux_amd64.zip + + echo "unzip nomad" + unzip -o -d /usr/local/bin/ /tmp/nomad_${NOMAD_VERSION}_linux_amd64.zip + chmod +x /usr/local/bin/nomad + /usr/local/bin/nomad version + - name: Install Dependencies + shell: bash + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements-dev.txt + - name: Before Tests + shell: bash + run: | + nomad init example.nomad + nomad run -output example.nomad > example.json + - name: Unit and Integration Tests + env: + NOMAD_VERSION: ${{ matrix.nomad-version }} + shell: bash + run: | + ./run_tests.sh + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b40fa75 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish + +on: + release: + types: + - created + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install Dependencies + shell: bash + run: pip install build + - name: Create Package + shell: bash + run: python -m build --sdist --wheel + - name: Publish/Release Package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 246fd60..ddeb60f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,8 @@ .vagrant .build .venv + +# dev related files +example.json +example.nomad +nomad.log diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..279e122 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,6 @@ +[FORMAT] +# Maximum number of characters on a single line. +max-line-length=120 + +[MESSAGES CONTROL] +disable=duplicate-code \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7d1b295..0000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -sudo: required - -deploy: - provider: pypi - user: jrxfive - password: - secure: KJFVOr1iqX1gh61IAzwztkn777Ya1bgN/Et+k5aI4hA+47n9kEPnoJLXP+Fak/OtwSx01oRElRgz/IP5y5yflLrnaTzLY4G7dpUsHf/bwOaWO4UEy43lnpTrjmhHEIUptHtufmue0FYSx4l/Os6XZYOZdUyJtLw+Zr72AuzMQlpgQhdwdjWngvI2FIX8gvoqda7rAJkyZyuW3vRJUdngDvzjWGhy8sRnZwCD56MLEbgnfD08Q2mSxbtHOb3B79EJdGZ3hqGV2AGdYuWfbf2q1yoUCH90AOCzKams8t+JCS/oRAOSlsG1iFMidpjtl+QKBashHrhJ/7xYNrmF49Hj6I8QFpZf4idUZQfo+uhoQd9YzzmEPb/Ys+PPD7Coy8RnD4xQpHQCMe6M+WJhEdFbXlNmjLLaNqm8Vfw9fnpWiFj3d64fZnr/TjTwKDLTVrITbEr95hs0jHx65VCon8ef2Yyt/w/rL66Nz7Skk7YzATloGaMNaennB7MzTxsTdjIvHmBFh2NlhRIAznYTzABewn7ktXHY6bAr7gIJRBr+2pdjQ+BMmtbvimHlUZ75xh94JpDv6vZKdyu2nkm9j444dwBEbCuqa8gPLasiik7OeKSoT0NtofwESb3lliHWsMDM1+kPx6aj1GYS1Kp7WndxVIKd3bEibhBgZY4JmuSWyuo= - on: - tags: true - repo: jrxFive/python-nomad - condition: $TRAVIS_PYTHON_VERSION = "2.7" - -services: -- docker -language: python -python: -- '2.7' -- '3.4' -- '3.5' -- '3.6' -env: - global: - - NOMAD_IP="127.0.0.1" - - NOMAD_PORT="4646" - matrix: - - NOMAD_VERSION="0.3.2" - - NOMAD_VERSION="0.4.1" - - NOMAD_VERSION="0.5.6" - - NOMAD_VERSION="0.6.0" - - NOMAD_VERSION="0.7.1" - - NOMAD_VERSION="0.8.1" - - NOMAD_VERSION="0.8.3" -before_install: -- curl -L -o /tmp/nomad_${NOMAD_VERSION}_linux_amd64.zip https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_linux_amd64.zip -- yes | unzip -d /tmp /tmp/nomad_${NOMAD_VERSION}_linux_amd64.zip -- MAJOR_VERSION=`echo ${NOMAD_VERSION} | cut -d "." -f 2` -- if [[ ${MAJOR_VERSION} -gt 6 ]]; then echo "Nomad version $NOMAD_VERSION supports acls";export ACL_ENABLED="--acl-enabled"; else echo "Nomad version $NOMAD_VERSION";export ACL_ENABLED=""; fi -- /tmp/nomad agent -dev -bind ${NOMAD_IP} -node pynomad1 ${ACL_ENABLED} > /dev/null 2>&1 & -- sleep 30 -install: -- pip install -r requirements.txt -r requirements-dev.txt -- pip install codecov -before_script: - - /tmp/nomad init - - /tmp/nomad run -output example.nomad > example.json -script: -- py.test --cov=nomad --cov-report=term-missing --runxfail tests/ -after_success: -- codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..56ebcdb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +**Changes of the next versions will be published in the [releases](https://github.com/jrxFive/python-nomad/releases) section of the repository** + +## 2.0.1 +* update parameters in `Job` functions by @IgnacioHeredia #153 +* Fix nodes API filter by @bernardoVale #162 +* Read env variables on __init__ by @atwam #157 + +## 2.0.0 +### BREAKING CHANGES +* Drop Python 2 and Python 3.6 support +* Rename `id` arguments to `id_` across of code base +* Rename `type` arguments to `type_` across of code base +### Other changes +* Add more missing parameters to allocations.get_allocations() +* Up `requests` lib version to 2.28.1 +* Add missing parameters to allocations.get_allocations and jobs.get_jobs (#144). Thanks @Kamilcuk +* Add option for custom user agent (#150) +* Add missing parameters to nodes.get_nodes (#152). +## 1.5.0 +* Add `namespace` argument support for `get_allocations` and `get_deployments` endpoints (#133) +* Add Python 3.10 support (#133) +* Add support for pre-populated Sessions (#132) +* Add scaling policy endpoint (#136) +* Drop Python 3.5 support +* Up `requests` lib version +* Add support for /var and /vars endpoints () +* Add support for /search endpoint (#134) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb4e933..f888149 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -180,7 +180,7 @@ class (Requester): ENDPOINT = "" def __init__(self, **kwargs): - super(, self).__init__(**kwargs) + super().__init__(**kwargs) ``` ##### Entity @@ -249,7 +249,7 @@ class cat(Requester): ENDPOINT = "client/fs/cat" def __init__(self, **kwargs): - super(cat, self).__init__(**kwargs) + super().__init__(**kwargs) def read_file(self, id=None, path="/"): """ Read contents of a file in an allocation directory. @@ -382,7 +382,7 @@ class (Requester): ENDPOINT = "" def __init__(self, **kwargs): - super(, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): return "{0}".format(self.__dict__) diff --git a/README.md b/README.md index 91497aa..05f7815 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # python-nomad -Branch | Status | Coverage | ----| ---| --- -master | [![Build Status](https://travis-ci.org/jrxFive/python-nomad.svg?branch=master)](https://travis-ci.org/jrxFive/python-nomad) | [![codecov](https://codecov.io/gh/jrxFive/python-nomad/branch/master/graph/badge.svg)](https://codecov.io/gh/jrxFive/python-nomad) +[![Python Nomad Test and Publish](https://github.com/jrxFive/python-nomad/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/jrxFive/python-nomad/actions/workflows/main.yml) +[![codecov](https://codecov.io/gh/jrxFive/python-nomad/branch/master/graph/badge.svg)](https://codecov.io/gh/jrxFive/python-nomad) +[![PyPI version](https://badge.fury.io/py/python-nomad.svg)](https://badge.fury.io/py/python-nomad) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/python-nomad.svg)](https://pypi.python.org/pypi/python-nomad/) +[![Downloads](https://pepy.tech/badge/python-nomad/month)](https://pepy.tech/project/python-nomad) +[![Downloads](https://static.pepy.tech/personalized-badge/python-nomad?period=total&units=international_system&left_color=black&right_color=blue&left_text=Downloads)](https://pepy.tech/project/python-nomad) +[![PyPI license](https://img.shields.io/pypi/l/python-nomad.svg)](https://pypi.python.org/pypi/python-nomad/) ## Installation @@ -34,6 +38,9 @@ n = nomad.Nomad(host="172.16.100.10", secure=True, timeout=5, verify=True, cert= # For HTTPS Nomad instances with cert file and key n = nomad.Nomad(host="https://172.16.100.10", secure=True, timeout=5, verify=True, cert=("/path/to/certfile", "/path/to/key")) # See http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification +# For HTTPS Nomad instance with cert file and key and CA file +n = nomad.Nomad(host="https://172.16.100.10", secure=True, timeout=5, verify="/path/to/cacert", cert=("/path/to/certfile", "/path/to/key")) + # For HTTPS Nomad instances with namespace and acl token n = nomad.Nomad(host="172.16.100.10", secure=True, timeout=5, verify=False, namespace='Namespace-example',token='3f4a0fcd-7c42-773c-25db-2d31ba0c05fe') @@ -48,14 +55,16 @@ n.job.deregister_job(j) ## Environment Variables -This library also supports environment variables: `NOMAD_ADDR`, `NOMAD_NAMESPACE`, `NOMAD_TOKEN`, `NOMAD_REGION` for ease of configuration -and unifying with nomad cli tools and other libraries. +This library also supports environment variables: `NOMAD_ADDR`, `NOMAD_NAMESPACE`, `NOMAD_TOKEN`, `NOMAD_REGION`, `NOMAD_CLIENT_CERT`, and `NOMAD_CLIENT_KEY` +for ease of configuration and unifying with nomad cli tools and other libraries. ```bash NOMAD_ADDR=http://127.0.0.1:4646 NOMAD_NAMESPACE=default NOMAD_TOKEN=xxxx-xxxx-xxxx-xxxx NOMAD_REGION=us-east-1a +NOMAD_CLIENT_CERT=/path/to/tls/client.crt +NOMAD_CLIENT_KEY=/path/to/tls/client.key ``` ## Class Dunders @@ -68,6 +77,7 @@ NOMAD_REGION=us-east-1a |client|N|N|N|N |evaluation|Y|N|Y|N |evaluations|Y|Y|Y|Y +|event|N|N|N|N |job|Y|N|Y|N |jobs|Y|Y|Y|Y |node|Y|N|Y|N @@ -90,8 +100,8 @@ NOMAD_REGION=us-east-1a * can either use the Vagrantfile for local integration testing or create environment variables `NOMAD_IP` and `NOMAD_PORT` that are assigned to a nomad binary that is running ``` -virutalenv venv -source venv/bin/activate +virtualenv .venv +source .venv/bin/activate pip install -r requirements-dev.txt ``` @@ -117,6 +127,7 @@ NOMAD_IP=127.0.0.1 NOMAD_VERSION= py.test --cov=nomad --cov-re - [x] Client [:link:](docs/api/client.md) - [x] Evaluation [:link:](docs/api/evaluation.md) - [x] Evaluations [:link:](docs/api/evaluations.md) + - [x] Event [:link:](docs/api/event.md) - [x] Job [:link:](docs/api/job.md) - [x] Jobs [:link:](docs/api/jobs.md) - [x] Namespace [:link:](docs/api/namespace.md) @@ -125,6 +136,9 @@ NOMAD_IP=127.0.0.1 NOMAD_VERSION= py.test --cov=nomad --cov-re - [x] Nodes [:link:](docs/api/nodes.md) - [x] Regions [:link:](docs/api/regions.md) - [x] Sentinel [:link:](docs/api/sentinel.md) + - [x] Search [:link:](docs/api/search.md) - [x] Status [:link:](docs/api/status.md) - [x] System [:link:](docs/api/system.md) - [x] Validate [:link:](docs/api/validate.md) + - [x] Variable [:link:](docs/api/variable.md) + - [x] Variables [:link:](docs/api/variables.md) diff --git a/docs/api/acl.md b/docs/api/acl.md index 34ec265..90fe636 100644 --- a/docs/api/acl.md +++ b/docs/api/acl.md @@ -8,7 +8,7 @@ Nomad must be running with ACL mode enabled. This endpoint is used to bootstrap the ACL system and provide the initial management token. This request is always forwarded to the authoritative region. It can only be invoked once until a bootstrap reset is performed. -https://www.nomadproject.io/api/acl-tokens.html#bootstrap-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#bootstrap-token Example: @@ -48,7 +48,7 @@ print (my_nomad.get_token()) This endpoint lists all ACL tokens. This lists the local tokens and the global tokens which have been replicated to the region, and may lag behind the authoritative region. -https://www.nomadproject.io/api/acl-tokens.html#list-tokens +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#list-tokens Exmaple: @@ -68,7 +68,7 @@ for token in tokens: This endpoint creates an ACL Token. If the token is a global token, the request is forwarded to the authoritative region. -https://www.nomadproject.io/api/acl-tokens.html#create-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#create-token Exmample: @@ -91,7 +91,7 @@ created_token = my_nomad.acl.create_token(new_token) This endpoint updates an existing ACL Token. If the token is a global token, the request is forwarded to the authoritative region. Note that a token cannot be switched from global to local or vice versa. -https://www.nomadproject.io/api/acl-tokens.html#update-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#update-token Example: @@ -115,7 +115,7 @@ updated_token = my_nomad.acl.update_token('377ba749-8b0e-c7fd-c0c0-9da5bb943088' This endpoint reads an ACL token with the given accessor. If the token is a global token which has been replicated to the region it may lag behind the authoritative region. -https://www.nomadproject.io/api/acl-tokens.html#read-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#read-token Exmaple: @@ -131,7 +131,7 @@ token = my_nomad.acl.get_token("377ba749-8b0e-c7fd-c0c0-9da5bb943088") This endpoint reads the ACL token given by the passed SecretID. If the token is a global token which has been replicated to the region it may lag behind the authoritative region. -https://www.nomadproject.io/api/acl-tokens.html#read-self-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#read-self-token Exmaple: @@ -147,7 +147,7 @@ self_token = my_nomad.acl.get_self_token() This endpoint deletes the ACL token by accessor. This request is forwarded to the authoritative region for global tokens. -https://www.nomadproject.io/api/acl-tokens.html#delete-token +https://developer.hashicorp.com/nomad/api-docs/acl/tokens#delete-token Example: @@ -164,13 +164,13 @@ my_nomad.acl.delete_token("377ba749-8b0e-c7fd-c0c0-9da5bb943088") Manage acl Policies -https://www.nomadproject.io/api/acl-policies.html +https://developer.hashicorp.com/nomad/api-docs/acl-policies.html ### List policies This endpoint lists all ACL policies. This lists the policies that have been replicated to the region, and may lag behind the authoritative region. -https://www.nomadproject.io/api/acl-policies.html#list-policies +https://developer.hashicorp.com/nomad/api-docs/acl-policies#list-policies Example: @@ -186,7 +186,7 @@ policies = my_nomad.acl.get_policies() This endpoint creates an ACL Policy. This request is always forwarded to the authoritative region. -https://www.nomadproject.io/api/acl-policies.html#create-or-update-policy +https://developer.hashicorp.com/nomad/api-docs/acl-policies#create-or-update-policy Example: ``` @@ -207,7 +207,7 @@ my_nomad.acl.create_policy("my-policy", policy) This endpoint update an ACL Policy. This request is always forwarded to the authoritative region. -https://www.nomadproject.io/api/acl-policies.html#create-or-update-policy +https://developer.hashicorp.com/nomad/api-docs/acl-policies#create-or-update-policy Example: @@ -229,7 +229,7 @@ my_nomad.acl.update_policy("my-policy", policy) This endpoint reads an ACL policy with the given name. This queries the policy that have been replicated to the region, and may lag behind the authoritative region. -https://www.nomadproject.io/api/acl-policies.html#read-policy +https://developer.hashicorp.com/nomad/api-docs/acl-policies#read-policy Example: diff --git a/docs/api/agent.md b/docs/api/agent.md index 32da2d2..81f623f 100644 --- a/docs/api/agent.md +++ b/docs/api/agent.md @@ -4,7 +4,7 @@ This endpoint queries the agent for the known peers in the gossip pool. This endpoint is only applicable to servers. Due to the nature of gossip, this is eventually consistent. -https://www.nomadproject.io/api/agent.html#list-members +https://developer.hashicorp.com/nomad/api-docs/agent#list-members Example: @@ -23,7 +23,7 @@ for member in members["Members"]: This endpoint lists the known server nodes. The servers endpoint is used to query an agent in client mode for its list of known servers. Client nodes register themselves with these server addresses so that they may dequeue work. The servers endpoint can be used to keep this configuration up to date if there are changes in the cluster -https://www.nomadproject.io/api/agent.html#list-servers +https://developer.hashicorp.com/nomad/api-docs/agent#list-servers Example: @@ -42,7 +42,7 @@ for server in servers: This endpoint queries the state of the target agent (self). -https://www.nomadproject.io/api/agent.html#query-self +https://developer.hashicorp.com/nomad/api-docs/agent#query-self Example: @@ -60,7 +60,7 @@ print (agent) This endpoint updates the list of known servers to the provided list. This replaces all previous server addresses with the new list. -https://www.nomadproject.io/api/agent.html#update-servers +https://developer.hashicorp.com/nomad/api-docs/agent#update-servers Example: @@ -76,7 +76,7 @@ r = my_nomad.agent.update_servers(['192.168.33.11', '10.1.10.200:4829']) This endpoint introduces a new member to the gossip pool. This endpoint is only eligible for servers. -https://www.nomadproject.io/api/agent.html#join-agent +https://developer.hashicorp.com/nomad/api-docs/agent#join-agent Example: @@ -92,7 +92,7 @@ r = my_nomad.agent.join_agent("server02") This endpoint forces a member of the gossip pool from the "failed" state to the "left" state. This allows the consensus protocol to remove the peer and stop attempting replication. This is only applicable for servers. -https://www.nomadproject.io/api/agent.html#force-leave-agent +https://developer.hashicorp.com/nomad/api-docs/agent#force-leave-agent Exmaple: @@ -110,7 +110,7 @@ This endpoint returns whether or not the agent is healthy. When using Consul it When the agent is unhealthy 500 will be returned along with JSON response containing an error message. -https://www.nomadproject.io/api/agent.html#health +https://developer.hashicorp.com/nomad/api-docs/agent#health Example: diff --git a/docs/api/allocation.md b/docs/api/allocation.md index 524817c..02efd9d 100644 --- a/docs/api/allocation.md +++ b/docs/api/allocation.md @@ -4,7 +4,7 @@ This endpoint reads information about a specific allocation. -https://www.nomadproject.io/api/allocations.html#read-allocation +https://developer.hashicorp.com/nomad/api-docs/allocations#read-allocation ``` import nomad @@ -15,3 +15,19 @@ allocation = my_nomad.allocation.get_allocation('32c54571-fb79-97d2-ee38-16673ba print (allocation) ``` + +### Stop an allocation + +This endpoint stops and reschedules a specific allocation. + +https://www.nomadproject.io/api-docs/allocations/#stop-allocation + +Example of stopping an allocation + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +my_nomad.allocation.stop_allocation('32c54571-fb79-97d2-ee38-16673bab692c') +``` diff --git a/docs/api/allocations.md b/docs/api/allocations.md new file mode 100644 index 0000000..1e6ac67 --- /dev/null +++ b/docs/api/allocations.md @@ -0,0 +1,43 @@ +## Allocations + +### List allocations + +This endpoint lists all allocations. + +https://developer.hashicorp.com/nomad/api-docs/allocations#list-allocations + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +allocations = my_nomad.allocations.get_allocations() + +for allocation in allocations: + print (allocation) +``` + +### Signal allocation + +This endpoint sends a signal to an allocation or task. + +https://developer.hashicorp.com/nomad/api-docs/allocations#signal-allocation + +Example: + +``` +import signal +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +alloc_id = nomad_setup.allocations.get_allocations()[0]["ID"] + +# Send signal to an allocation +my_nomad.client.allocation.signal_allocation(alloc_id, signal.SIGUSR1.name) + +# Send signal to a task in allocation +my_nomad.client.allocation.signal_allocation(alloc_id, signal.SIGUSR1.name, task="my_task") +``` diff --git a/docs/api/alocations.md b/docs/api/alocations.md deleted file mode 100644 index 8c7ef2f..0000000 --- a/docs/api/alocations.md +++ /dev/null @@ -1,20 +0,0 @@ -## Allocations - -### List allocations - -This endpoint lists all allocations. - -https://www.nomadproject.io/api/allocations.html#list-allocations - -Example: - -``` -import nomad - -my_nomad = nomad.Nomad(host='192.168.33.10') - -allocations = my_nomad.allocations.get_allocations() - -for allocation in allocations: - print (allocation) -``` diff --git a/docs/api/client.md b/docs/api/client.md index da9ca15..bd7746f 100644 --- a/docs/api/client.md +++ b/docs/api/client.md @@ -4,7 +4,7 @@ This endpoint queries the actual resources consumed on a node. The API endpoint is hosted by the Nomad client and requests have to be made to the nomad client whose resource usage metrics are of interest. -https://www.nomadproject.io/api/client.html#read-stats +https://developer.hashicorp.com/nomad/api-docs/client#read-stats Example: diff --git a/docs/api/deployment.md b/docs/api/deployment.md index f5ae5a9..919bbcd 100644 --- a/docs/api/deployment.md +++ b/docs/api/deployment.md @@ -6,7 +6,7 @@ The deployment endpoints are used to query for and interact with deployments. This endpoint reads information about a specific deployment by ID. -https://www.nomadproject.io/api/deployments.html#read-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#read-deployment Example: @@ -25,7 +25,7 @@ print (deployment) This endpoint lists the allocations created or modified for the given deployment. -https://www.nomadproject.io/api/deployments.html#list-allocations-for-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#list-allocations-for-deployment Example: @@ -44,7 +44,7 @@ for allocation in allocations: This endpoint is used to mark a deployment as failed. This should be done to force the scheduler to stop creating allocations as part of the deployment or to cause a rollback to a previous job version. This endpoint only triggers a rollback if the most recent stable version of the job has a different specification than the job being reverted. -https://www.nomadproject.io/api/deployments.html#fail-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#fail-deployment example: @@ -69,7 +69,7 @@ fail_deployment = my_nomad.deployment.fail_deployment('a8061a1c-d4c9-2a7d-a4b2-9 This endpoint is used to pause or unpause a deployment. This is done to pause a rolling upgrade or resume it. -https://www.nomadproject.io/api/deployments.html#pause-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#pause-deployment example: @@ -103,7 +103,7 @@ pause = my_nomad.deployment.pause_deployment("52c47d49-eefa-540f-f0f1-d25ba298c8 This endpoint is used to promote task groups that have canaries for a deployment. This should be done when the placed canaries are healthy and the rolling upgrade of the remaining allocations should begin. -https://www.nomadproject.io/api/deployments.html#promote-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#promote-deployment #### Promote All @@ -116,7 +116,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') -promote = my_nomad.deployment.pause_deployment("52c47d49-eefa-540f-f0f1-d25ba298c87f",True) +promote = my_nomad.deployment.promote_deployment_all("52c47d49-eefa-540f-f0f1-d25ba298c87f",True) ``` #### Promote Groups @@ -130,7 +130,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') -promote = my_nomad.deployment.pause_deployment("52c47d49-eefa-540f-f0f1-d25ba298c87f",groups=['task1','task2']) +promote = my_nomad.deployment.promote_deployment_groups("52c47d49-eefa-540f-f0f1-d25ba298c87f",groups=['task1','task2']) ``` @@ -138,7 +138,7 @@ promote = my_nomad.deployment.pause_deployment("52c47d49-eefa-540f-f0f1-d25ba298 This endpoint is used to set the health of an allocation that is in the deployment manually. In some use cases, automatic detection of allocation health may not be desired. As such those task groups can be marked with an upgrade policy that uses health_check = "manual". Those allocations must have their health marked manually using this endpoint. Marking an allocation as healthy will allow the rolling upgrade to proceed. Marking it as failed will cause the deployment to fail. This endpoint only triggers a rollback if the most recent stable version of the job has a different specification than the job being reverted. -https://www.nomadproject.io/api/deployments.html#set-allocation-health-in-deployment +https://developer.hashicorp.com/nomad/api-docs/deployments#set-allocation-health-in-deployment example: diff --git a/docs/api/deployments.md b/docs/api/deployments.md index ab5bdcf..4cc0a03 100644 --- a/docs/api/deployments.md +++ b/docs/api/deployments.md @@ -6,7 +6,7 @@ The deployment endpoints are used to query for and interact with deployments. This endpoint lists all deployments. -https://www.nomadproject.io/api/deployments.html#list-deployments +https://developer.hashicorp.com/nomad/api-docs/deployments#list-deployments Example: diff --git a/docs/api/evaluation.md b/docs/api/evaluation.md index e035a83..561dc98 100644 --- a/docs/api/evaluation.md +++ b/docs/api/evaluation.md @@ -4,7 +4,7 @@ This endpoint reads information about a specific evaluation by ID. -https://www.nomadproject.io/api/evaluations.html#read-evaluation +https://developer.hashicorp.com/nomad/api-docs/evaluations#read-evaluation Example: @@ -22,7 +22,7 @@ print (evaluation) This endpoint lists the allocations created or modified for the given evaluation. -https://www.nomadproject.io/api/evaluations.html#list-allocations-for-evaluation +https://developer.hashicorp.com/nomad/api-docs/evaluations#list-allocations-for-evaluation Example: diff --git a/docs/api/evaluations.md b/docs/api/evaluations.md index f0745cd..fcf48b3 100644 --- a/docs/api/evaluations.md +++ b/docs/api/evaluations.md @@ -4,7 +4,7 @@ This endpoint lists all evaluations. -https://www.nomadproject.io/api/evaluations.html#list-evaluations +https://developer.hashicorp.com/nomad/api-docs/evaluations#list-evaluations Example: diff --git a/docs/api/event.md b/docs/api/event.md new file mode 100644 index 0000000..7b235a3 --- /dev/null +++ b/docs/api/event.md @@ -0,0 +1,81 @@ +## Event + +## Event Stream + +This will setup an event stream. To avoid blocking and having more control to the user it will return a +tuple of (threading.Thread, threading.Event and queue.Queue). You can use your own `queue.Queue` if you want +to use LIFO or SimpleQueue or simply extend upon that. + +### Default +Will listen to all topics + +``` +import nomad +n = nomad.Nomad() + +stream, stream_exit_event, events = n.event.stream.get_stream() +stream.start() + +while True: + event = events.get() + print(event) + events.task_done() +``` + +### Set Index, Namespace and Topic(s) of Interest + +``` +import nomad +n = nomad.Nomad() + +stream, stream_exit_event, events = n.event.stream.get_stream(index=0, topic={"Node": "*"}, namespace="not-default") +stream.start() + +while True: + event = events.get() + print(event) + events.task_done() +``` + +### Cancel thread/Optimistically exit +We will use the `stream_exit_event` to get the thread to return/exit gracefully. This isn't immediate +as we have to wait for an event or set an arbitrary timeout value to close/open the connection again. + +In this example we will set `stream_exit_event` right before the timeout, knowing that it needs to re-establish +the connection to the stream. Using a try/except with queue.Queue.get(timeout=) we will check if the thread +is still alive; if it isn't we break the loop. + +``` +import nomad +import threading +import time +import queue + + +def stop_stream(exit_event, timeout): + print("start sleep") + time.sleep(timeout) + print("set exit event") + exit_event.set() + + +n = nomad.Nomad() + +stream, stream_exit_event, events = n.event.stream.get_stream(index=0, topic={"Node": "*"}, timeout=3.2) +stream.start() + +stop = threading.Thread(target=stop_stream, args=(stream_exit_event, 3.0)) +stop.start() + +while True: + if not stream.is_alive(): + print("not alive") + break + + try: + event = events.get(timeout=1.0) + print(event) + events.task_done() + except queue.Empty: + continue +``` \ No newline at end of file diff --git a/docs/api/job.md b/docs/api/job.md index b9ddf42..af6f77f 100644 --- a/docs/api/job.md +++ b/docs/api/job.md @@ -4,7 +4,7 @@ This endpoint creates (aka "registers") a new job in the system. -https://www.nomadproject.io/api/jobs.html#create-job +https://developer.hashicorp.com/nomad/api-docs/jobs#create-job Example: @@ -109,7 +109,7 @@ response = my_nomad.job.register_job("example", job) This endpoint reads information about a single job for its specification and status. -https://www.nomadproject.io/api/jobs.html#read-job +https://developer.hashicorp.com/nomad/api-docs/jobs#read-job Example: @@ -126,7 +126,7 @@ job = my_nomad.job.get_job("example") This endpoint reads information about all versions of a job. -https://www.nomadproject.io/api/jobs.html#list-job-versions +https://developer.hashicorp.com/nomad/api-docs/jobs#list-job-versions Example: @@ -145,7 +145,7 @@ for version in versions["Versions"]: This endpoint reads information about a single job's allocations. -https://www.nomadproject.io/api/jobs.html#list-job-allocations +https://developer.hashicorp.com/nomad/api-docs/jobs#list-job-allocations Example: @@ -164,7 +164,7 @@ for allocation in allocations: This endpoint reads information about a single job's evaluations -https://www.nomadproject.io/api/jobs.html#list-job-evaluations +https://developer.hashicorp.com/nomad/api-docs/jobs#list-job-evaluations Example: @@ -180,11 +180,11 @@ for evaluation in evaluations: ``` -### List job deploymetns +### List job deployments This endpoint lists a single job's deployments -https://www.nomadproject.io/api/jobs.html#list-job-deployments +https://developer.hashicorp.com/nomad/api-docs/jobs#list-job-deployments Example: @@ -204,7 +204,7 @@ for deployment in deployments: This endpoint returns a single job's most recent deployment. -https://www.nomadproject.io/api/jobs.html#read-job-39-s-most-recent-deployment +https://developer.hashicorp.com/nomad/api-docs/jobs#read-job-39-s-most-recent-deployment Example: @@ -221,7 +221,7 @@ deployment = my_nomad.job.get_deployment("example") This endpoint reads summary information about a job. -https://www.nomadproject.io/api/jobs.html#read-job-summary +https://developer.hashicorp.com/nomad/api-docs/jobs#read-job-summary Example: @@ -238,7 +238,7 @@ summary = my_nomad.job.get_summary("example") This endpoint registers a new job or updates an existing job. -https://www.nomadproject.io/api/jobs.html#update-existing-job +https://developer.hashicorp.com/nomad/api-docs/jobs#update-existing-job Example: @@ -249,7 +249,7 @@ See create new job This endpoint dispatches a new instance of a parameterized job. -https://www.nomadproject.io/api/jobs.html#dispatch-job +https://developer.hashicorp.com/nomad/api-docs/jobs#dispatch-job Example: @@ -287,7 +287,7 @@ parametrize_job = { "Name": "example-task", "Driver": "docker", "Config": { - "args": ["${NOMAD_META_TIME"], + "args": ["${NOMAD_META_TIME}"], "command": "sleep", "image": "scratch", "logging": [], @@ -340,7 +340,7 @@ my_nomad.job.dispatch_job("example-batch", meta={"time": "500"}) This endpoint reverts the job to an older version. -https://www.nomadproject.io/api/jobs.html#revert-to-older-job-version +https://developer.hashicorp.com/nomad/api-docs/jobs#revert-to-older-job-version Example: @@ -349,7 +349,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') -prior_job_version = my_nomad.job.job.get_deployment("example")["JobVersion"] +current_job_version = my_nomad.job.job.get_deployment("example")["JobVersion"] prior_job_version = current_job_version - 1 @@ -361,7 +361,7 @@ my_nomad.job.revert_job("example", prior_job_version, current_job_version) This endpoint sets the job's stability. -https://www.nomadproject.io/api/jobs.html#set-job-stability +https://developer.hashicorp.com/nomad/api-docs/jobs#set-job-stability Example: @@ -380,7 +380,7 @@ my_nomad.job.stable_job("example", current_job_version, True) This endpoint creates a new evaluation for the given job. This can be used to force run the scheduling logic if necessary. -https://www.nomadproject.io/api/jobs.html#create-job-evaluation +https://developer.hashicorp.com/nomad/api-docs/jobs#create-job-evaluation Example: @@ -396,7 +396,7 @@ my_nomad.job.evaluate_job("example") This endpoint invokes a dry-run of the scheduler for the job. -https://www.nomadproject.io/api/jobs.html#create-job-plan +https://developer.hashicorp.com/nomad/api-docs/jobs#create-job-plan Example: @@ -500,7 +500,7 @@ plan = my_nomad.job.plan_job("example", job) This endpoint deregisters a job, and stops all allocations part of it. -https://www.nomadproject.io/api/jobs.html#stop-a-job +https://developer.hashicorp.com/nomad/api-docs/jobs#stop-a-job Example of deferred removal of job (performed by Nomad garbage collector): diff --git a/docs/api/jobs.md b/docs/api/jobs.md index 08fb1f6..c1a94a5 100644 --- a/docs/api/jobs.md +++ b/docs/api/jobs.md @@ -4,7 +4,7 @@ This endpoint lists all known jobs in the system registered with Nomad. -https://www.nomadproject.io/api/jobs.html#list-jobs +https://developer.hashicorp.com/nomad/api-docs/jobs#list-jobs Example: @@ -23,7 +23,7 @@ for job in jobs: This endpoint creates (aka "registers") a new job in the system. -https://www.nomadproject.io/api/jobs.html#create-job +https://developer.hashicorp.com/nomad/api-docs/jobs#create-job Example: @@ -129,7 +129,7 @@ To convert to python dict and verify for correctness a hcl/nomad file. The examp `nomad job init` and it will assume this file is in the current working directory. In practice this file should already be read and used as the parameter hcl. -https://www.nomadproject.io/api/jobs.html#parse-job +https://developer.hashicorp.com/nomad/api-docs/jobs#parse-job ```python diff --git a/docs/api/metrics.md b/docs/api/metrics.md index f797525..5c5fe0c 100644 --- a/docs/api/metrics.md +++ b/docs/api/metrics.md @@ -2,7 +2,7 @@ ### Get node metrics -https://www.nomadproject.io/api/metrics.html +https://developer.hashicorp.com/nomad/api-docs/metrics.html Example: diff --git a/docs/api/namespace.md b/docs/api/namespace.md index c4d8a42..57ea1ed 100644 --- a/docs/api/namespace.md +++ b/docs/api/namespace.md @@ -8,7 +8,7 @@ You must have nomad **ENTERPRISE Edition** Create new namespace -https://www.nomadproject.io/api/namespaces.html#create-or-update-namespace +https://developer.hashicorp.com/nomad/api-docs/namespaces#create-or-update-namespace Exmample: @@ -18,7 +18,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') namespace = { - "Namespace": "api-prod", + "Name": "api-prod", "Description": "Production API Servers" } my_nomad.namespace.create_namespace(namespace) @@ -36,7 +36,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') namespace = { - "Namespace": "api-prod", + "Name": "api-prod", "Description": "Production API Servers" } my_nomad.namespace.create_namespace(namespace) @@ -52,7 +52,7 @@ print (my_nomad.get_namespace()) This endpoint reads information about a specific namespace. -https://www.nomadproject.io/api/namespaces.html#read-namespace +https://developer.hashicorp.com/nomad/api-docs/namespaces#read-namespace Exmample: @@ -69,7 +69,7 @@ namespace = my_nomad.namespace.get_namespace("api-prod") Update existing namespace -https://www.nomadproject.io/api/namespaces.html#create-or-update-namespace +https://developer.hashicorp.com/nomad/api-docs/namespaces#create-or-update-namespace Example: @@ -79,7 +79,7 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') namespace = { - "Namespace": "api-prod", + "Name": "api-prod", "Description": "Production API Servers" } my_nomad.namespace.create_namespace("api-prod", namespace) @@ -89,7 +89,7 @@ my_nomad.namespace.create_namespace("api-prod", namespace) Delete namespace -https://www.nomadproject.io/api/namespaces.html#create-or-update-namespace +https://developer.hashicorp.com/nomad/api-docs/namespaces#create-or-update-namespace Exmaple: diff --git a/docs/api/namespaces.md b/docs/api/namespaces.md index bb2595b..359ced3 100644 --- a/docs/api/namespaces.md +++ b/docs/api/namespaces.md @@ -8,7 +8,7 @@ You must have nomad **ENTERPRISE Edition** This endpoint lists all namespaces. -https://www.nomadproject.io/api/namespaces.html#list-namespaces +https://developer.hashicorp.com/nomad/api-docs/namespaces#list-namespaces Exmaple: diff --git a/docs/api/node.md b/docs/api/node.md index 91e3d1d..f89ef69 100644 --- a/docs/api/node.md +++ b/docs/api/node.md @@ -4,7 +4,7 @@ This endpoint queries the status of a client node. -https://www.nomadproject.io/api/nodes.html#read-node +https://developer.hashicorp.com/nomad/api-docs/nodes#read-node Example: @@ -20,7 +20,7 @@ node = my_nomad.node.get_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98') This endpoint lists all of the allocations for the given node. This can be used to determine what allocations have been scheduled on the node, their current status, and the values of dynamically assigned resources, like ports. -https://www.nomadproject.io/api/nodes.html#list-node-allocations +https://developer.hashicorp.com/nomad/api-docs/nodes#list-node-allocations Example: @@ -40,7 +40,7 @@ for allocation in allocations: This endpoint creates a new evaluation for the given node. This can be used to force a run of the scheduling logic. -https://www.nomadproject.io/api/nodes.html#create-node-evaluation +https://developer.hashicorp.com/nomad/api-docs/nodes#create-node-evaluation Example: @@ -56,7 +56,7 @@ my_nomad.node.evaluate_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98') This endpoint toggles the drain mode of the node. When draining is enabled, no further allocations will be assigned to this node, and existing allocations will be migrated to new nodes. -https://www.nomadproject.io/api/nodes.html#drain-node +https://developer.hashicorp.com/nomad/api-docs/nodes#drain-node Example: @@ -76,7 +76,7 @@ my_nomad.node.drain_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', enable=False) This endpoint toggles the drain mode of the node. When draining is enabled, no further allocations will be assigned to this node, and existing allocations will be migrated to new nodes. -https://www.nomadproject.io/api/nodes.html#drain-node +https://developer.hashicorp.com/nomad/api-docs/nodes#drain-node Example: @@ -86,23 +86,23 @@ import nomad my_nomad = nomad.Nomad(host='192.168.33.10') #enable drain mode -my_nomad.node.drain_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={"Duration": "100000000"}) +my_nomad.node.drain_node_with_spec('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={"Duration": "100000000"}) #enable drain mode but leave system jobs on the specificed node -my_nomad.node.drain_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={"Duration": "100000000", "IgnoreSystemJobs": True}) +my_nomad.node.drain_node_with_spec('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={"Duration": "100000000", "IgnoreSystemJobs": True}) #disable drain but leave node in an ineligible state -my_nomad.node.drain_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={}) +my_nomad.node.drain_node_with_spec('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={}) #disable drain and put node in an eligible state -my_nomad.node.drain_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={}, mark_eligible=True) +my_nomad.node.drain_node_with_spec('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', drain_spec={}, mark_eligible=True) ``` ### Eligible Node This endpoint toggles the eligibility of the node. When a node's "SchedulingEligibility" is ineligible the scheduler will not consider it for new placements. -https://www.nomadproject.io/api/nodes.html#toggle-node-eligibility +https://developer.hashicorp.com/nomad/api-docs/nodes#toggle-node-eligibility Example: @@ -122,7 +122,7 @@ my_nomad.node.eligible_node('ed1bbae7-c38a-df2d-1de7-50dbc753fc98', eligible=Tru This endpoint purges a node from the system. Nodes can still join the cluster if they are alive. -https://www.nomadproject.io/api/nodes.html#purge-node +https://developer.hashicorp.com/nomad/api-docs/nodes#purge-node Example: diff --git a/docs/api/nodes.md b/docs/api/nodes.md index 03cbc78..825957e 100644 --- a/docs/api/nodes.md +++ b/docs/api/nodes.md @@ -4,7 +4,7 @@ This endpoint lists all nodes registered with Nomad. -https://www.nomadproject.io/api/nodes.html#list-nodes +https://developer.hashicorp.com/nomad/api-docs/nodes#list-nodes Example: diff --git a/docs/api/regions.md b/docs/api/regions.md index 54b7754..f68c230 100644 --- a/docs/api/regions.md +++ b/docs/api/regions.md @@ -2,7 +2,7 @@ ### List regions -https://www.nomadproject.io/api/regions.html#list-regions +https://developer.hashicorp.com/nomad/api-docs/regions#list-regions Example: diff --git a/docs/api/search.md b/docs/api/search.md new file mode 100644 index 0000000..7fab18a --- /dev/null +++ b/docs/api/search.md @@ -0,0 +1,37 @@ +## Search + +### Regular search + +https://developer.hashicorp.com/nomad/api-docs/search + +Search in context (can be: jobs, evals, allocs, nodes, deployment, plugins, volumes or all) with prefix + +`all` context means every context will be searched. + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +metrics = my_nomad.search("test", "jobs") +``` + +### Fuzzy search + +https://developer.hashicorp.com/nomad/api-docs/search#fuzzy-searching + +Search any text in context (can be: jobs, allocs, nodes, plugins, or all) + +`all` context means every context will be searched. + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +metrics = my_nomad.search.fuzzy("test", "jobs") +``` \ No newline at end of file diff --git a/docs/api/sentinel.md b/docs/api/sentinel.md index 2693c28..1fe7148 100644 --- a/docs/api/sentinel.md +++ b/docs/api/sentinel.md @@ -8,7 +8,7 @@ You must have nomad **ENTERPRISE Edition** Get all policies -https://www.nomadproject.io/api/sentinel-policies.html#list-policies +https://developer.hashicorp.com/nomad/api-docs/sentinel-policies#list-policies Example: @@ -24,7 +24,7 @@ policies = my_nomad.sentinel.get_policies() Create a policy -https://www.nomadproject.io/api/sentinel-policies.html#create-or-update-policy +https://developer.hashicorp.com/nomad/api-docs/sentinel-policies#create-or-update-policy Example: ``` @@ -47,7 +47,7 @@ my_nomad.sentinel.create_policy("my-policy", policy) Update specific policy -https://www.nomadproject.io/api/sentinel-policies.html#create-or-update-policy +https://developer.hashicorp.com/nomad/api-docs/sentinel-policies#create-or-update-policy Example: @@ -71,7 +71,7 @@ my_nomad.sentinel.update_policy("my-policy", policy) Get specific policy -https://www.nomadproject.io/api/sentinel-policies.html#read-policy +https://developer.hashicorp.com/nomad/api-docs/sentinel-policies#read-policy Example: @@ -87,7 +87,7 @@ policy = my_nomad.sentinel.get_policy("my-policy") Delete specific policy -https://www.nomadproject.io/api/sentinel-policies.html#delete-policy +https://developer.hashicorp.com/nomad/api-docs/sentinel-policies#delete-policy Example: diff --git a/docs/api/status.md b/docs/api/status.md index ed5cc97..6e30d5d 100644 --- a/docs/api/status.md +++ b/docs/api/status.md @@ -4,7 +4,7 @@ This endpoint returns the address of the current leader in the region. -https://www.nomadproject.io/api/status.html#read-leader +https://developer.hashicorp.com/nomad/api-docs/status#read-leader Example: @@ -20,7 +20,7 @@ leader = my_nomad.status.leader.get_leader() This endpoint returns the set of raft peers in the region. -https://www.nomadproject.io/api/status.html#list-peers +https://developer.hashicorp.com/nomad/api-docs/status#list-peers Example: diff --git a/docs/api/system.md b/docs/api/system.md index ead5bc0..f836fe6 100644 --- a/docs/api/system.md +++ b/docs/api/system.md @@ -4,7 +4,7 @@ This endpoint initializes a garbage collection of jobs, evaluations, allocations, and nodes. This is an asynchronous operation. -https://www.nomadproject.io/api/system.html#force-gc +https://developer.hashicorp.com/nomad/api-docs/system#force-gc Example: @@ -20,7 +20,7 @@ my_nomad.system.initiate_garbage_collection() This endpoint reconciles the summaries of all registered jobs. -https://www.nomadproject.io/api/system.html#reconcile-summaries +https://developer.hashicorp.com/nomad/api-docs/system#reconcile-summaries Example: diff --git a/docs/api/validate.md b/docs/api/validate.md index 8b84022..ce05d91 100644 --- a/docs/api/validate.md +++ b/docs/api/validate.md @@ -4,7 +4,7 @@ This endpoint validates a Nomad job file. The local Nomad agent forwards the request to a server. In the event a server can't be reached the agent verifies the job file locally but skips validating driver configurations. -https://www.nomadproject.io/api/validate.html#validate-job +https://developer.hashicorp.com/nomad/api-docs/validate#validate-job Example: diff --git a/docs/api/variable.md b/docs/api/variable.md new file mode 100644 index 0000000..a4139bc --- /dev/null +++ b/docs/api/variable.md @@ -0,0 +1,54 @@ +## Variable + +### Read variable + +This endpoint reads a specific variable by path. This API returns the decrypted variable body. + +https://developer.hashicorp.com/nomad/api-docs/variables + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +variable = my_nomad.variable.get_variable("path_to_variable") +``` + +### Create variable + +This endpoint creates or updates a variable. + +https://developer.hashicorp.com/nomad/api-docs/variables + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +job = my_nomad.variable.create_variable("path_to_variable") + +payload = { + "Items": {"user": "test", "password": "test123"}, +} +my_nomad.variable.create_variable("variable_path", payload) +``` + +### Delete variable + +This endpoint deletes a specific variable by path. + +https://developer.hashicorp.com/nomad/api-docs/variables + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +my_nomad.variable.delete_variable("path_to_variable") +``` \ No newline at end of file diff --git a/docs/api/variables.md b/docs/api/variables.md new file mode 100644 index 0000000..b1c3abe --- /dev/null +++ b/docs/api/variables.md @@ -0,0 +1,20 @@ +## Variables + +### List variables + +This endpoint lists all known variables in the system registered with Nomad. + +https://developer.hashicorp.com/nomad/api-docs/variables + +Example: + +``` +import nomad + +my_nomad = nomad.Nomad(host='192.168.33.10') + +variables = my_nomad.variables.get_variables() + +for var in variables: + print(var) +``` diff --git a/docs/index.md b/docs/index.md index de10bc5..af1fad4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -57,6 +57,7 @@ NOMAD_REGION=us-east-1a |client|N|N|N|N |evaluation|Y|N|Y|N |evaluations|Y|Y|Y|Y +|event|N|N|N|N |job|Y|N|Y|N |jobs|Y|Y|Y|Y |node|Y|N|Y|N @@ -88,4 +89,4 @@ pip install -r requirements-dev.txt ``` vagrant up --provider virtualbox py.test --cov=nomad --cov-report=term-missing --runxfail tests/ -``` \ No newline at end of file +``` diff --git a/example_batch_parameterized.json b/example_batch_parameterized.json index 1be8e91..9e100b2 100644 --- a/example_batch_parameterized.json +++ b/example_batch_parameterized.json @@ -1,6 +1,6 @@ { "Job": { - "Region": "example-region", + "Region": "global", "ID": "example-batch", "ParentID": "", "Name": "example-batch", diff --git a/mkdocs.yml b/mkdocs.yml index 9bc017a..cf1f47a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,7 @@ pages: - Deployments: 'api/deployments.md' - Evaluations: 'api/evaluations.md' - Evaluation: 'api/evaluation.md' + - Event: 'api/event.md' - Job: 'api/job.md' - Jobs: 'api/jobs.md' - Metrics: 'api/metrics.md' @@ -24,9 +25,12 @@ pages: - Regions: 'api/regions.md' - Status: 'api/status.md' - System: 'api/system.md' + - Search: 'api/search.md' - Acl: 'api/acl.md' - Namespace: 'api/namespace.md' - Namespaces: 'api/namespaces.md' - Sentinel: 'api/sentinel.md' + - Variable: 'api/variable.md' + - Variables: 'api/variables.md' - Validate: 'api/validate.md' - Contributing: 'CONTRIBUTING.md' diff --git a/nomad/__init__.py b/nomad/__init__.py index 0eb04bf..43437c5 100644 --- a/nomad/__init__.py +++ b/nomad/__init__.py @@ -1,192 +1,332 @@ +"""Nomad Python library""" -import nomad.api as api import os - -class Nomad(object): - - def __init__(self, - host='127.0.0.1', - secure=False, - port=4646, - address=os.getenv('NOMAD_ADDR', None), - namespace=os.getenv('NOMAD_NAMESPACE', None), - token=os.getenv('NOMAD_TOKEN', None), - timeout=5, - region=os.getenv('NOMAD_REGION', None), - version='v1', - verify=False, - cert=()): - """ Nomad api client - - https://github.com/jrxFive/python-nomad/ - - optional arguments: - - host (defaults 127.0.0.1), string ip or name of the nomad api server/agent that will be used. - - port (defaults 4646), integer port that will be used to connect. - - secure (defaults False), define if the protocol is secured or not (https or http) - - version (defaults v1), vesion of the api of nomad. - - verify (defaults False), verify the certificate when tls/ssl is enabled - at nomad. - - cert (defaults empty), cert, or key and cert file to validate the certificate - configured at nomad. - - region (defaults None), version of the region to use. It will be used then - regions of the current agent of the connection. - - namespace (defaults to None), Specifies the enterpise namespace that will - be use to deploy or to ask info to nomad. - - token (defaults to None), Specifies to append ACL token to the headers to - make authentication on secured based nomad environemnts. - returns: Nomad api client object - - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException - - nomad.api.exceptions.URLNotAuthorizedNomadException +from typing import Optional, Union + +import requests + +from nomad import api + + +class Nomad: # pylint: disable=too-many-public-methods,too-many-instance-attributes + """ + Nomad API + """ + + def __init__( # pylint: disable=too-many-arguments + self, + host: str = "127.0.0.1", + secure: bool = False, + port: int = 4646, + address: Optional[str] = None, + user_agent: Optional[str] = None, + namespace: Optional[str] = None, + token: Optional[str] = None, + timeout: int = 5, + region: Optional[str] = None, + version: str = "v1", + verify: Union[bool, str] = False, + cert: tuple = (), + session: requests.Session = None, + ): + """Nomad api client + + https://github.com/jrxFive/python-nomad/ + + optional arguments: + - host (defaults 127.0.0.1), string ip or name of the nomad api server/agent that will be used. + - port (defaults 4646), integer port that will be used to connect. + - user_agent (defaults None), custom user agent for requests to Nomad. + - secure (defaults False), define if the protocol is secured or not (https or http) + - version (defaults v1), version of the api of nomad. + - verify (defaults False), verify SSL certificates for HTTPS requests. Can be a boolean to enable/disable + verification, or a string path to a CA certificate file. + - cert (defaults empty), cert, or key and cert file to validate the certificate + configured at nomad. + - region (defaults None), version of the region to use. It will be used then + regions of the current agent of the connection. + - namespace (defaults to None), Specifies the enterprise namespace that will + be use to deploy or to ask info to nomad. + - token (defaults to None), Specifies to append ACL token to the headers to + make authentication on secured based nomad environments. + - session (defaults to None), allows for injecting a prepared requests.Session object that + all requests to Nomad should use. + returns: Nomad api client object + + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + - nomad.api.exceptions.URLNotAuthorizedNomadException """ + cert = cert or ( + os.getenv("NOMAD_CLIENT_CERT", None), + os.getenv("NOMAD_CLIENT_KEY", None), + ) + self.host = host self.secure = secure self.port = port - self.address = address - self.region = region + self.address = address or os.getenv("NOMAD_ADDR", None) + self.user_agent = user_agent + self.region = region or os.getenv("NOMAD_REGION", None) self.timeout = timeout self.version = version - self.token = token + self.token = token or os.getenv("NOMAD_TOKEN", None) self.verify = verify - self.cert = cert - self.__namespace = namespace + self.cert = cert if all(cert) else () + self.session = session + self.__namespace = namespace or os.getenv("NOMAD_NAMESPACE", None) self.requester_settings = { "address": self.address, "uri": self.get_uri(), "port": self.port, + "user_agent": self.user_agent, "namespace": self.__namespace, "token": self.token, "timeout": self.timeout, "version": self.version, "verify": self.verify, "cert": self.cert, - "region": self.region + "region": self.region, + "session": self.session, } - self._jobs = api.Jobs(**self.requester_settings) - self._job = api.Job(**self.requester_settings) - self._nodes = api.Nodes(**self.requester_settings) - self._node = api.Node(**self.requester_settings) - self._allocations = api.Allocations(**self.requester_settings) - self._allocation = api.Allocation(**self.requester_settings) - self._evaluations = api.Evaluations(**self.requester_settings) - self._evaluation = api.Evaluation(**self.requester_settings) + self._acl = api.Acl(**self.requester_settings) self._agent = api.Agent(**self.requester_settings) + self._allocation = api.Allocation(**self.requester_settings) + self._allocations = api.Allocations(**self.requester_settings) self._client = api.Client(**self.requester_settings) - self._deployments = api.Deployments(**self.requester_settings) self._deployment = api.Deployment(**self.requester_settings) + self._deployments = api.Deployments(**self.requester_settings) + self._evaluation = api.Evaluation(**self.requester_settings) + self._evaluations = api.Evaluations(**self.requester_settings) + self._event = api.Event(**self.requester_settings) + self._job = api.Job(**self.requester_settings) + self._jobs = api.Jobs(**self.requester_settings) + self._metrics = api.Metrics(**self.requester_settings) + self._namespace = api.Namespace(**self.requester_settings) + self._namespaces = api.Namespaces(**self.requester_settings) + self._node = api.Node(**self.requester_settings) + self._nodes = api.Nodes(**self.requester_settings) + self._operator = api.Operator(**self.requester_settings) self._regions = api.Regions(**self.requester_settings) + self._scaling = api.Scaling(**self.requester_settings) + self._sentinel = api.Sentinel(**self.requester_settings) + self._search = api.Search(**self.requester_settings) self._status = api.Status(**self.requester_settings) self._system = api.System(**self.requester_settings) - self._operator = api.Operator(**self.requester_settings) self._validate = api.Validate(**self.requester_settings) - self._namespaces = api.Namespaces(**self.requester_settings) - self._namespace = api.Namespace(**self.requester_settings) - self._acl = api.Acl(**self.requester_settings) - self._sentinel = api.Sentinel(**self.requester_settings) - self._metrics = api.Metrics(**self.requester_settings) + self._variable = api.Variable(**self.requester_settings) + self._variables = api.Variables(**self.requester_settings) def get_uri(self): + """ + Get Nomad host + """ if self.secure: protocol = "https" else: protocol = "http" - return "{protocol}://{host}".format(protocol=protocol, host=self.host) + return f"{protocol}://{self.host}" def get_namespace(self): + """ + Get Nomad namaspace + """ return self.__namespace def get_token(self): + """ + Get Nomad token + """ return self.token @property def jobs(self): + """ + Jobs API + """ return self._jobs @property def job(self): + """ + Job API + """ return self._job @property def nodes(self): + """ + Nodes API + """ return self._nodes @property def node(self): + """ + Node API + """ return self._node @property def allocations(self): + """ + Allocations API + """ return self._allocations @property def allocation(self): + """ + Allocation API + """ return self._allocation @property def evaluations(self): + """ + Evaluations API + """ return self._evaluations @property def evaluation(self): + """ + Evaluation API + """ return self._evaluation + @property + def event(self): + """ + Event API + """ + return self._event + @property def agent(self): + """ + Agent API + """ return self._agent @property def client(self): + """ + Client API + """ return self._client @property def deployments(self): + """ + Deployments API + """ return self._deployments @property def deployment(self): + """ + Deployment API + """ return self._deployment @property def regions(self): + """ + Regions API + """ return self._regions + @property + def scaling(self): + """ + Scaling API + """ + return self._scaling + @property def status(self): + """ + Status API + """ return self._status @property def system(self): + """ + System API + """ return self._system @property def operator(self): + """ + Operator API + """ return self._operator @property def validate(self): + """ + Validate API + """ return self._validate @property def namespaces(self): + """ + Namespaces API + """ return self._namespaces @property def namespace(self): + """ + Namespace API + """ return self._namespace @property def acl(self): + """ + ACL API + """ return self._acl @property def sentinel(self): + """ + Sentinel API + """ return self._sentinel + @property + def search(self): + """ + Search API + """ + return self._search + @property def metrics(self): + """ + Metrics API + """ return self._metrics + + @property + def variable(self): + """ + Variable API + """ + return self._variable + + @property + def variables(self): + """ + Variables API + """ + return self._variables diff --git a/nomad/api/__init__.py b/nomad/api/__init__.py index b91f125..0c3f17e 100644 --- a/nomad/api/__init__.py +++ b/nomad/api/__init__.py @@ -1,24 +1,31 @@ +"""Nomad Python library""" + import nomad.api.exceptions -from nomad.api.base import Requester -from nomad.api.jobs import Jobs -from nomad.api.job import Job -from nomad.api.nodes import Nodes -from nomad.api.node import Node +from nomad.api.acl import Acl from nomad.api.agent import Agent -from nomad.api.allocations import Allocations from nomad.api.allocation import Allocation -from nomad.api.evaluations import Evaluations -from nomad.api.evaluation import Evaluation +from nomad.api.allocations import Allocations +from nomad.api.base import Requester from nomad.api.client import Client +from nomad.api.deployment import Deployment +from nomad.api.deployments import Deployments +from nomad.api.evaluation import Evaluation +from nomad.api.evaluations import Evaluations +from nomad.api.event import Event +from nomad.api.job import Job +from nomad.api.jobs import Jobs +from nomad.api.metrics import Metrics +from nomad.api.namespace import Namespace +from nomad.api.namespaces import Namespaces +from nomad.api.node import Node +from nomad.api.nodes import Nodes +from nomad.api.operator import Operator from nomad.api.regions import Regions +from nomad.api.scaling import Scaling +from nomad.api.sentinel import Sentinel +from nomad.api.search import Search from nomad.api.status import Status from nomad.api.system import System -from nomad.api.operator import Operator from nomad.api.validate import Validate -from nomad.api.deployments import Deployments -from nomad.api.deployment import Deployment -from nomad.api.namespaces import Namespaces -from nomad.api.namespace import Namespace -from nomad.api.acl import Acl -from nomad.api.sentinel import Sentinel -from nomad.api.metrics import Metrics +from nomad.api.variable import Variable +from nomad.api.variables import Variables diff --git a/nomad/api/acl.py b/nomad/api/acl.py index af23e80..9b3b024 100644 --- a/nomad/api/acl.py +++ b/nomad/api/acl.py @@ -1,4 +1,4 @@ -import nomad.api.exceptions +"""Nomad ACL: https://developer.hashicorp.com/nomad/api-docs/acl""" from nomad.api.base import Requester @@ -13,182 +13,182 @@ class Acl(Requester): ENDPOINT = "acl" def __init__(self, **kwargs): - super(Acl, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): raise AttributeError def generate_bootstrap(self): - """ Activate bootstrap token. + """Activate bootstrap token. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - returns: dict + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("bootstrap", method="post").json() def get_tokens(self): - """ Get a list of tokens. + """Get a list of tokens. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - returns: list + returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("tokens", method="get").json() - def get_token(self, id): - """ Retrieve specific token. + def get_token(self, id_): + """Retrieve specific token. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - returns: dict + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("token", id, method="get").json() + return self.request("token", id_, method="get").json() def get_self_token(self): - """ Retrieve self token used for auth. + """Retrieve self token used for auth. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - returns: dict + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("token", "self", method="get").json() def create_token(self, token): - """ Create token. + """Create token. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - arguments: - token - returns: dict + arguments: + token + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("token", json=token, method="post").json() - def delete_token(self, id): - """ Delete specific token. + def delete_token(self, id_): + """Delete specific token. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - returns: Boolean + returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("token", id, method="delete").ok + return self.request("token", id_, method="delete").ok - def update_token(self, id, token): - """ Update token. + def update_token(self, id_, token): + """Update token. - https://www.nomadproject.io/api/acl-tokens.html + https://www.nomadproject.io/api/acl-tokens.html - arguments: - - AccdesorID - - token - returns: dict + arguments: + - AccdesorID + - token + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("token", id, json=token, method="post").json() + return self.request("token", id_, json=token, method="post").json() def get_policies(self): - """ Get a list of policies. + """Get a list of policies. - https://www.nomadproject.io/api/acl-policies.html + https://www.nomadproject.io/api/acl-policies.html - returns: list + returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("policies", method="get").json() - def create_policy(self, id, policy): - """ Create policy. + def create_policy(self, id_, policy): + """Create policy. - https://www.nomadproject.io/api/acl-policies.html + https://www.nomadproject.io/api/acl-policies.html - arguments: - - policy - returns: request.Response + arguments: + - policy + returns: request.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, json=policy, method="post") + return self.request("policy", id_, json=policy, method="post") - def get_policy(self, id): - """ Get a spacific. + def get_policy(self, id_): + """Get a spacific. - https://www.nomadproject.io/api/acl-policies.html + https://www.nomadproject.io/api/acl-policies.html - returns: dict + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, method="get").json() + return self.request("policy", id_, method="get").json() - def update_policy(self, id, policy): - """ Create policy. + def update_policy(self, id_, policy): + """Create policy. - https://www.nomadproject.io/api/acl-policies.html + https://www.nomadproject.io/api/acl-policies.html - arguments: - - name - - policy - returns: request.Response + arguments: + - name + - policy + returns: request.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, json=policy, method="post") + return self.request("policy", id_, json=policy, method="post") - def delete_policy(self, id): - """ Delete specific policy. + def delete_policy(self, id_): + """Delete specific policy. - https://www.nomadproject.io/api/acl-policies.html + https://www.nomadproject.io/api/acl-policies.html - arguments: - - id - returns: Boolean + arguments: + - id + returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, method="delete").ok + return self.request("policy", id_, method="delete").ok diff --git a/nomad/api/agent.py b/nomad/api/agent.py index c954c29..c62babe 100644 --- a/nomad/api/agent.py +++ b/nomad/api/agent.py @@ -1,85 +1,85 @@ -import nomad.api.exceptions +"""Nomad Agent: https://developer.hashicorp.com/nomad/api-docs/agent""" from nomad.api.base import Requester class Agent(Requester): - """The self endpoint is used to query the state of the target agent.""" + ENDPOINT = "agent" def __init__(self, **kwargs): - super(Agent, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def get_agent(self): - """ Query the state of the target agent. + """Query the state of the target agent. - https://www.nomadproject.io/docs/http/agent-self.html + https://www.nomadproject.io/docs/http/agent-self.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("self", method="get").json() def get_members(self): """Lists the known members of the gossip pool. - https://www.nomadproject.io/docs/http/agent-members.html + https://www.nomadproject.io/docs/http/agent-members.html - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("members", method="get").json() def get_servers(self): - """ Lists the known members of the gossip pool. + """Lists the known members of the gossip pool. - https://www.nomadproject.io/docs/http/agent-servers.html + https://www.nomadproject.io/docs/http/agent-servers.html - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("servers", method="get").json() def join_agent(self, addresses): """Initiate a join between the agent and target peers. - https://www.nomadproject.io/docs/http/agent-join.html + https://www.nomadproject.io/docs/http/agent-join.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"address": addresses} return self.request("join", params=params, method="post").json() def update_servers(self, addresses): """Updates the list of known servers to the provided list. - Replaces all previous server addresses with the new list. + Replaces all previous server addresses with the new list. - https://www.nomadproject.io/docs/http/agent-servers.html + https://www.nomadproject.io/docs/http/agent-servers.html - returns: 200 status code - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: 200 status code + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"address": addresses} return self.request("servers", params=params, method="post").status_code @@ -87,12 +87,12 @@ def update_servers(self, addresses): def force_leave(self, node): """Force a failed gossip member into the left state. - https://www.nomadproject.io/docs/http/agent-force-leave.html + https://www.nomadproject.io/docs/http/agent-force-leave.html - returns: 200 status code - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: 200 status code + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"node": node} return self.request("force-leave", params=params, method="post").status_code diff --git a/nomad/api/allocation.py b/nomad/api/allocation.py index 30b23ef..9e9da20 100644 --- a/nomad/api/allocation.py +++ b/nomad/api/allocation.py @@ -1,3 +1,5 @@ +"""Nomad allocation: https://developer.hashicorp.com/nomad/api-docs/allocations""" + import nomad.api.exceptions from nomad.api.base import Requester @@ -15,13 +17,13 @@ class Allocation(Requester): ENDPOINT = "allocation" def __init__(self, **kwargs): - super(Allocation, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): raise AttributeError @@ -32,6 +34,7 @@ def __contains__(self, item): if response["ID"] == item: return True + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -41,17 +44,30 @@ def __getitem__(self, item): if response["ID"] == item: return response - except nomad.api.exceptions.URLNotFoundNomadException: raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc + + def get_allocation(self, id_: str): + """Query a specific allocation. + + https://www.nomadproject.io/docs/http/alloc.html + + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + return self.request(id_, method="get").json() - def get_allocation(self, id): - """ Query a specific allocation. + def stop_allocation(self, id_: str): + """Stop a specific allocation. - https://www.nomadproject.io/docs/http/alloc.html + https://www.nomadproject.io/api-docs/allocations/#stop-allocation - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + return self.request(id_, "stop", method="post").json() diff --git a/nomad/api/allocations.py b/nomad/api/allocations.py index eca4ec2..920beaa 100644 --- a/nomad/api/allocations.py +++ b/nomad/api/allocations.py @@ -1,8 +1,11 @@ +"""Nomad allocation: https://developer.hashicorp.com/nomad/api-docs/allocations""" + +from typing import Optional + from nomad.api.base import Requester class Allocations(Requester): - """ The allocations endpoint is used to query the status of allocations. By default, the agent's local region is used; another region can be @@ -10,16 +13,17 @@ class Allocations(Requester): https://www.nomadproject.io/docs/http/allocs.html """ + ENDPOINT = "allocations" def __init__(self, **kwargs): - super(Allocations, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): raise AttributeError @@ -32,17 +36,48 @@ def __iter__(self): response = self.get_allocations() return iter(response) - def get_allocations(self, prefix=None): - """ Lists all the allocations. - - https://www.nomadproject.io/docs/http/allocs.html - arguments: - - prefix :(str) optional, specifies a string to filter allocations on based on an prefix. - This is specified as a querystring parameter. - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + def get_allocations( # pylint: disable=too-many-arguments + self, + prefix: Optional[str] = None, + next_token: Optional[str] = None, + per_page: Optional[int] = None, + filter_: Optional[str] = None, + namespace: Optional[str] = None, + resources: Optional[bool] = None, + task_states: Optional[bool] = None, + reverse: Optional[bool] = None, + ): + """Lists all the allocations. + + https://www.nomadproject.io/docs/http/allocs.html + arguments: + - prefix :(str) optional, specifies a string to filter allocations on based on an prefix. + This is specified as a querystring parameter. + - next_token :(str) optional. + This endpoint supports paging. The next_token parameter accepts a string which identifies the next + expected allocation. This value can be obtained from the X-Nomad-NextToken header from the previous + response. + - per_page :(int) optional + - filter_ :(str) optional + Name has a trailing underscore not to conflict with builtin function. + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. + - resources :(bool) optional + - task_states :(bool) optional + - reverse :(bool) optional + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - params = {"prefix": prefix} + params = { + "prefix": prefix, + "next_token": next_token, + "per_page": per_page, + "filter": filter_, + "namespace": namespace, + "resources": resources, + "task_states": task_states, + "reverse": reverse, + } return self.request(method="get", params=params).json() diff --git a/nomad/api/base.py b/nomad/api/base.py index cca3a09..ef340fe 100644 --- a/nomad/api/base.py +++ b/nomad/api/base.py @@ -1,18 +1,37 @@ -import requests -import nomad.api.exceptions +"""Requester""" + +from typing import Optional, Union -from requests.packages.urllib3.exceptions import InsecureRequestWarning +import requests -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) +import nomad.api.exceptions -class Requester(object): +class Requester: # pylint: disable=too-many-instance-attributes,too-few-public-methods + """ + Base object for endpoints + """ ENDPOINT = "" - def __init__(self, address=None, uri='http://127.0.0.1', port=4646, namespace=None, token=None, timeout=5, version='v1', verify=False, cert=(), region=None, **kwargs): + def __init__( # pylint: disable=too-many-arguments + self, + address: Optional[str] = None, + uri: Optional[str] = "http://127.0.0.1", + port: int = 4646, + user_agent: Optional[str] = None, + namespace: Optional[str] = None, + token: Optional[str] = None, + timeout: int = 5, + version: str = "v1", + verify: Union[bool, str] = False, + cert: tuple = (), + region: Optional[str] = None, + session: requests.Session = None, + ): self.uri = uri self.port = port + self.user_agent = user_agent self.namespace = namespace self.token = token self.timeout = timeout @@ -20,31 +39,37 @@ def __init__(self, address=None, uri='http://127.0.0.1', port=4646, namespace=No self.verify = verify self.cert = cert self.address = address - self.session = requests.Session() + self.session = session or requests.Session() self.region = region def _endpoint_builder(self, *args): if args: - u = "/".join(args) - return "{v}/".format(v=self.version) + u + args_str = "/".join(args) + return f"{self.version}/" + args_str + + return "/" def _required_namespace(self, endpoint): required_namespace = [ - "job", - "jobs", - "allocation", - "allocations", - "deployment", - "deployments", - "acl" - ] + "job", + "jobs", + "allocation", + "allocations", + "deployment", + "deployments", + "acl", + "client", + "node", + "variable", + "variables", + ] # split 0 -> Api Version # split 1 -> Working Endpoint - ENDPOINT_NAME = 1 endpoint_split = endpoint.split("/") try: - required = endpoint_split[ENDPOINT_NAME] in required_namespace - except: + endpoint_name = 1 + required = endpoint_split[endpoint_name] in required_namespace + except IndexError: required = False return required @@ -53,24 +78,32 @@ def _url_builder(self, endpoint): url = self.address if self.address is None: - url = "{uri}:{port}".format(uri=self.uri, port=self.port) - - url = "{url}/{endpoint}".format(url=url, endpoint=endpoint) + url = f"{self.uri}:{self.port}" + url = f"{url}/{endpoint}" return url - def _query_string_builder(self, endpoint): - qs = {} + def _query_string_builder(self, endpoint, params=None): + query_string = {} + + if not isinstance(params, dict): + params = {} + + # Remove parameters that are None + params = {key: val for key, val in params.items() if val is not None} - if self.namespace and self._required_namespace(endpoint): - qs["namespace"] = self.namespace + if ("namespace" not in params) and (self.namespace and self._required_namespace(endpoint)): + query_string["namespace"] = self.namespace - if self.region: - qs["region"] = self.region + if "region" not in params and self.region: + query_string["region"] = self.region - return qs + return query_string def request(self, *args, **kwargs): + """ + Send HTTP Request (wrapper around requests) + """ endpoint = self._endpoint_builder(self.ENDPOINT, *args) response = self._request( endpoint=endpoint, @@ -79,84 +112,109 @@ def request(self, *args, **kwargs): data=kwargs.get("data", None), json=kwargs.get("json", None), headers=kwargs.get("headers", None), - allow_redirects=kwargs.get("allow_redirects", False) + allow_redirects=kwargs.get("allow_redirects", False), + timeout=kwargs.get("timeout", self.timeout), + stream=kwargs.get("stream", False), ) return response - def _request(self, method, endpoint, params=None, data=None, json=None, headers=None, allow_redirects=None): + def _request( # pylint: disable=too-many-arguments, too-many-branches + self, + method, + endpoint, + params=None, + data=None, + json=None, + headers=None, + allow_redirects=None, + timeout=None, + stream=False, + ): url = self._url_builder(endpoint) - qs = self._query_string_builder(endpoint) + query_string = self._query_string_builder(endpoint=endpoint, params=params) if params: - params.update(qs) + params.update(query_string) else: - params = qs + params = query_string if self.token: - try: + if headers is not None: headers["X-Nomad-Token"] = self.token - except TypeError: + else: headers = {"X-Nomad-Token": self.token} + if self.user_agent: + headers["User-Agent"] = self.user_agent + response = None try: method = method.lower() if method == "get": response = self.session.get( - url=url, - params=params, + allow_redirects=allow_redirects, + cert=self.cert, headers=headers, - timeout=self.timeout, + params=params, + stream=stream, + timeout=timeout, + url=url, verify=self.verify, - cert=self.cert, - allow_redirects=allow_redirects ) elif method == "post": response = self.session.post( - url=url, - params=params, - json=json, - headers=headers, + allow_redirects=allow_redirects, + cert=self.cert, data=data, - timeout=self.timeout, + headers=headers, + json=json, + params=params, + timeout=timeout, + url=url, verify=self.verify, - cert=self.cert, - allow_redirects=allow_redirects ) elif method == "put": response = self.session.put( - url=url, - params=params, - json=json, - headers=headers, + cert=self.cert, data=data, + headers=headers, + json=json, + params=params, + timeout=timeout, + url=url, verify=self.verify, - cert=self.cert, - timeout=self.timeout ) elif method == "delete": response = self.session.delete( - url=url, - params=params, + cert=self.cert, headers=headers, + params=params, + timeout=timeout, + url=url, verify=self.verify, - cert=self.cert, - timeout=self.timeout ) if response.ok: return response - elif response.status_code == 400: + if response.status_code == 400: raise nomad.api.exceptions.BadRequestNomadException(response) - elif response.status_code == 403: + if response.status_code == 403: raise nomad.api.exceptions.URLNotAuthorizedNomadException(response) - elif response.status_code == 404: + if response.status_code == 404: raise nomad.api.exceptions.URLNotFoundNomadException(response) - else: - raise nomad.api.exceptions.BaseNomadException(response) + if response.status_code == 409: + raise nomad.api.exceptions.VariableConflict(response) - except requests.RequestException: raise nomad.api.exceptions.BaseNomadException(response) + + except requests.exceptions.ConnectionError as error: + if all([stream, timeout]): + raise nomad.api.exceptions.TimeoutNomadException(error) + + raise nomad.api.exceptions.BaseNomadException(error) + + except requests.RequestException as error: + raise nomad.api.exceptions.BaseNomadException(error) diff --git a/nomad/api/client.py b/nomad/api/client.py index f7784ba..9f4374c 100644 --- a/nomad/api/client.py +++ b/nomad/api/client.py @@ -1,7 +1,13 @@ +# we want to have backward compatibility here +# pylint: disable=invalid-name,too-many-instance-attributes,too-many-arguments +"""Nomad Client: https://developer.hashicorp.com/nomad/api-docs/client""" from nomad.api.base import Requester -class Client(object): +class Client: + """ + The /client endpoints are used to interact with the Nomad clients. + """ def __init__(self, **kwargs): self.ls = ls(**kwargs) @@ -16,17 +22,17 @@ def __init__(self, **kwargs): self.gc_all_allocations = gc_all_allocations(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) class ls(Requester): - """ The /fs/ls endpoint is used to list files in an allocation directory. This API endpoint is hosted by the Nomad client and requests have to be @@ -38,29 +44,28 @@ class ls(Requester): ENDPOINT = "client/fs/ls" def __init__(self, **kwargs): - super(ls, self).__init__(**kwargs) + super().__init__(**kwargs) - def list_files(self, id=None, path="/"): - """ List files in an allocation directory. + def list_files(self, id_=None, path="/"): + """List files in an allocation directory. - https://www.nomadproject.io/docs/http/client-fs-ls.html + https://www.nomadproject.io/docs/http/client-fs-ls.html - arguments: - - id - - path - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + - path + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - if id: - return self.request(id, params={"path": path}, method="get").json() - else: - return self.request(params={"path": path}, method="get").json() + if id_: + return self.request(id_, params={"path": path}, method="get").json() + return self.request(params={"path": path}, method="get").json() -class cat(Requester): +class cat(Requester): """ The /fs/cat endpoint is used to read the contents of a file in an allocation directory. This API endpoint is hosted by the Nomad @@ -73,29 +78,28 @@ class cat(Requester): ENDPOINT = "client/fs/cat" def __init__(self, **kwargs): - super(cat, self).__init__(**kwargs) + super().__init__(**kwargs) - def read_file(self, id=None, path="/"): - """ Read contents of a file in an allocation directory. + def read_file(self, id_=None, path="/"): + """Read contents of a file in an allocation directory. - https://www.nomadproject.io/docs/http/client-fs-cat.html + https://www.nomadproject.io/docs/http/client-fs-cat.html - arguments: - - id - - path - returns: (str) text - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + - path + returns: (str) text + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - if id: - return self.request(id, params={"path": path}, method="get").text - else: - return self.request(params={"path": path}, method="get").text + if id_: + return self.request(id_, params={"path": path}, method="get").text + return self.request(params={"path": path}, method="get").text -class read_at(Requester): +class read_at(Requester): """ This endpoint reads the contents of a file in an allocation directory at a particular offset and limit. @@ -105,33 +109,28 @@ class read_at(Requester): ENDPOINT = "client/fs/readat" def __init__(self, **kwargs): - super(read_at, self).__init__(**kwargs) - - def read_file_offset(self, id, offset, limit, path="/"): - """ Read contents of a file in an allocation directory. - - https://www.nomadproject.io/docs/http/client-fs-cat.html - - arguments: - - id: (str) allocation_id required - - offset: (int) required - - limit: (int) required - - path: (str) optional - returns: (str) text - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.BadRequestNomadException + super().__init__(**kwargs) + + def read_file_offset(self, id_, offset, limit, path="/"): + """Read contents of a file in an allocation directory. + + https://www.nomadproject.io/docs/http/client-fs-cat.html + + arguments: + - id_: (str) allocation_id required + - offset: (int) required + - limit: (int) required + - path: (str) optional + returns: (str) text + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.BadRequestNomadException """ - params = { - "path": path, - "offset": offset, - "limit": limit - } - return self.request(id, params=params, method="get").text + params = {"path": path, "offset": offset, "limit": limit} + return self.request(id_, params=params, method="get").text class stream_file(Requester): - """ This endpoint streams the contents of a file in an allocation directory. @@ -141,33 +140,28 @@ class stream_file(Requester): ENDPOINT = "client/fs/stream" def __init__(self, **kwargs): - super(stream_file, self).__init__(**kwargs) - - def stream(self, id, offset, origin, path="/"): - """ This endpoint streams the contents of a file in an allocation directory. - - https://www.nomadproject.io/api/client.html#stream-file - - arguments: - - id: (str) allocation_id required - - offset: (int) required - - origin: (str) either start|end - - path: (str) optional - returns: (str) text - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.BadRequestNomadException + super().__init__(**kwargs) + + def stream(self, id_, offset, origin, path="/"): + """This endpoint streams the contents of a file in an allocation directory. + + https://www.nomadproject.io/api/client.html#stream-file + + arguments: + - id_: (str) allocation_id required + - offset: (int) required + - origin: (str) either start|end + - path: (str) optional + returns: (str) text + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.BadRequestNomadException """ - params = { - "path": path, - "offset": offset, - "origin": origin - } - return self.request(id, params=params, method="get").text + params = {"path": path, "offset": offset, "origin": origin} + return self.request(id_, params=params, method="get").text class stream_logs(Requester): - """ This endpoint streams a task's stderr/stdout logs. @@ -177,41 +171,40 @@ class stream_logs(Requester): ENDPOINT = "client/fs/logs" def __init__(self, **kwargs): - super(stream_logs, self).__init__(**kwargs) - - def stream(self, id, task, type, follow=False, offset=0, origin="start", plain=False): - """ This endpoint streams a task's stderr/stdout logs. - - https://www.nomadproject.io/api/client.html#stream-logs - - arguments: - - id: (str) allocation_id required - - task: (str) name of the task inside the allocation to stream logs from - - type: (str) Specifies the stream to stream. Either "stderr|stdout" - - follow: (bool) default false - - offset: (int) default 0 - - origin: (str) either start|end, default "start" - - plain: (bool) Return just the plain text without framing. default False - returns: (str) text - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.BadRequestNomadException + super().__init__(**kwargs) + + def stream(self, id_, task, type_, follow=False, offset=0, origin="start", plain=False): + """This endpoint streams a task's stderr/stdout logs. + + https://www.nomadproject.io/api/client.html#stream-logs + + arguments: + - id_: (str) allocation_id required + - task: (str) name of the task inside the allocation to stream logs from + - type_: (str) Specifies the stream to stream. Either "stderr|stdout" + - follow: (bool) default false + - offset: (int) default 0 + - origin: (str) either start|end, default "start" + - plain: (bool) Return just the plain text without framing. default False + returns: (str) text + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.BadRequestNomadException """ params = { "task": task, - "type": type, + "type": type_, "follow": follow, "offset": offset, "origin": origin, - "plain": plain + "plain": plain, } - return self.request(id, params=params, method="get").text + return self.request(id_, params=params, method="get").text class stat(Requester): - """ - The /fs/stat endpoint is used to show stat information + The /fs/stat endpoint is used to show stat information This API endpoint is hosted by the Nomad client and requests have to be made to the Nomad client where the particular allocation was placed. @@ -221,29 +214,28 @@ class stat(Requester): ENDPOINT = "client/fs/stat" def __init__(self, **kwargs): - super(stat, self).__init__(**kwargs) + super().__init__(**kwargs) - def stat_file(self, id=None, path="/"): - """ Stat a file in an allocation directory. + def stat_file(self, id_=None, path="/"): + """Stat a file in an allocation directory. - https://www.nomadproject.io/docs/http/client-fs-stat.html + https://www.nomadproject.io/docs/http/client-fs-stat.html - arguments: - - id - - path - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + - path + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - if id: - return self.request(id, params={"path": path}, method="get").json() - else: - return self.request(params={"path": path}, method="get").json() + if id_: + return self.request(id_, params={"path": path}, method="get").json() + return self.request(params={"path": path}, method="get").json() -class stats(Requester): +class stats(Requester): """ The /stats endpoint queries the actual resources consumed on a node. The API endpoint is hosted by the Nomad client and requests have to @@ -255,28 +247,28 @@ class stats(Requester): ENDPOINT = "client/stats" def __init__(self, **kwargs): - super(stats, self).__init__(**kwargs) + super().__init__(**kwargs) def read_stats(self, node_id=None): - """ Query the actual resources consumed on a node. + """ + Query the actual resources consumed on a node. - https://www.nomadproject.io/api/client.html#read-stats + https://www.nomadproject.io/api/client.html#read-stats - arguments: - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(params={"node_id": node_id}, method="get").json() class allocation(Requester): - """ The allocation/:alloc_id/stats endpoint is used to query the actual - resources consumed by an allocation. The API endpoint is hosted by the - Nomad client and requests have to be made to the nomad client whose + resources consumed by an allocation. The API endpoint is hosted by the + Nomad client and requests have to be made to the nomad client whose resource usage metrics are of interest. https://www.nomadproject.io/api/client.html#read-allocation @@ -285,24 +277,51 @@ class allocation(Requester): ENDPOINT = "client/allocation" def __init__(self, **kwargs): - super(allocation, self).__init__(**kwargs) + super().__init__(**kwargs) - def read_allocation_stats(self, id): - """ Query the actual resources consumed by an allocation. + def read_allocation_stats(self, id_): + """Query the actual resources consumed by an allocation. - https://www.nomadproject.io/api/client.html#read-allocation + https://www.nomadproject.io/api/client.html#read-allocation - arguments: - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "stats", method="get").json() + return self.request(id_, "stats", method="get").json() + def restart_allocation(self, id_): + """Restart a specific allocation. -class gc_allocation(Requester): + https://www.nomadproject.io/api-docs/allocations/#restart-allocation + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + return self.request(id_, "restart", method="post").json() + + def signal_allocation(self, id_, signal, task=None): + """Send a signal to an allocation or task. + https://www.nomadproject.io/api-docs/allocations#signal-allocation + arguments: + - id_ + - signal (str) + optional_arguments: + - task: (str) Optional, if omitted, the signal will be sent to all tasks in the allocation. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + payload = {"Signal": signal, "Task": task} + return self.request(id_, "signal", json=payload, method="post").json() + + +class gc_allocation(Requester): """ This endpoint forces a garbage collection of a particular, stopped allocation on a node. @@ -312,24 +331,23 @@ class gc_allocation(Requester): ENDPOINT = "client/allocation" def __init__(self, **kwargs): - super(gc_allocation, self).__init__(**kwargs) + super().__init__(**kwargs) - def garbage_collect(self, id): - """ This endpoint forces a garbage collection of a particular, stopped allocation on a node. + def garbage_collect(self, id_): + """This endpoint forces a garbage collection of a particular, stopped allocation on a node. - https://www.nomadproject.io/api/client.html#gc-allocation + https://www.nomadproject.io/api/client.html#gc-allocation - arguments: - - id: (str) full allocation_id - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_: (str) full allocation_id + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - self.request(id, "gc", method="get") + self.request(id_, "gc", method="get") class gc_all_allocations(Requester): - """ This endpoint forces a garbage collection of all stopped allocations on a node. @@ -339,16 +357,16 @@ class gc_all_allocations(Requester): ENDPOINT = "client/gc" def __init__(self, **kwargs): - super(gc_all_allocations, self).__init__(**kwargs) + super().__init__(**kwargs) def garbage_collect(self, node_id=None): - """ This endpoint forces a garbage collection of all stopped allocations on a node. + """This endpoint forces a garbage collection of all stopped allocations on a node. - https://www.nomadproject.io/api/client.html#gc-all-allocation + https://www.nomadproject.io/api/client.html#gc-all-allocation - arguments: - - node_id: (str) full allocation_id - raises: - - nomad.api.exceptions.BaseNomadException + arguments: + - node_id: (str) full allocation_id + raises: + - nomad.api.exceptions.BaseNomadException """ self.request(params={"node_id": node_id}, method="get") diff --git a/nomad/api/deployment.py b/nomad/api/deployment.py index 238bf2f..ff7ac3b 100644 --- a/nomad/api/deployment.py +++ b/nomad/api/deployment.py @@ -1,165 +1,172 @@ +"""Nomad Deployment: https://developer.hashicorp.com/nomad/api-docs/deployments""" + import nomad.api.exceptions from nomad.api.base import Requester class Deployment(Requester): - """ The /deployment endpoints are used to query for and interact with deployments. https://www.nomadproject.io/docs/http/deployments.html """ + ENDPOINT = "deployment" def __init__(self, **kwargs): - super(Deployment, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): - try: - d = self.get_deployment(item) + self.get_deployment(item) return True except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): - try: - d = self.get_deployment(item) - - if d["ID"] == item: - return d - except nomad.api.exceptions.URLNotFoundNomadException: + deployment = self.get_deployment(item) + if deployment["ID"] == item: + return deployment raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exp: + raise KeyError from exp - def get_deployment(self, id): - """ This endpoint reads information about a specific deployment by ID. + def get_deployment(self, id_): + """This endpoint reads information about a specific deployment by ID. - https://www.nomadproject.io/docs/http/deployments.html + https://www.nomadproject.io/docs/http/deployments.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + return self.request(id_, method="get").json() - def get_deployment_allocations(self, id): - """ This endpoint lists the allocations created or modified for the given deployment. + def get_deployment_allocations(self, id_): + """This endpoint lists the allocations created or modified for the given deployment. - https://www.nomadproject.io/docs/http/deployments.html + https://www.nomadproject.io/docs/http/deployments.html - arguments: - - id - returns: list of dicts - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("allocations", id, method="get").json() + return self.request("allocations", id_, method="get").json() - def fail_deployment(self, id): - """ This endpoint is used to mark a deployment as failed. This should be done to force the scheduler to stop - creating allocations as part of the deployment or to cause a rollback to a previous job version. + def fail_deployment(self, id_): + """This endpoint is used to mark a deployment as failed. This should be done to force the scheduler to stop + creating allocations as part of the deployment or to cause a rollback to a previous job version. - https://www.nomadproject.io/docs/http/deployments.html + https://www.nomadproject.io/docs/http/deployments.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - fail_json = {"DeploymentID": id} - return self.request("fail", id, json=fail_json, method="post").json() - - def pause_deployment(self, id, pause): - """ This endpoint is used to pause or unpause a deployment. - This is done to pause a rolling upgrade or resume it. - - https://www.nomadproject.io/docs/http/deployments.html - - arguments: - - id - - pause, Specifies whether to pause or resume the deployment. - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + fail_json = {"DeploymentID": id_} + return self.request("fail", id_, json=fail_json, method="post").json() + + def pause_deployment(self, id_, pause): + """This endpoint is used to pause or unpause a deployment. + This is done to pause a rolling upgrade or resume it. + + https://www.nomadproject.io/docs/http/deployments.html + + arguments: + - id_ + - pause, Specifies whether to pause or resume the deployment. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - pause_json = {"Pause": pause, - "DeploymentID": id} - return self.request("pause", id, json=pause_json, method="post").json() - - def promote_deployment_all(self, id, all=True): - """ This endpoint is used to promote task groups that have canaries for a deployment. This should be done when - the placed canaries are healthy and the rolling upgrade of the remaining allocations should begin. - - https://www.nomadproject.io/docs/http/deployments.html - - arguments: - - id - - all, Specifies whether all task groups should be promoted. - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + pause_json = {"Pause": pause, "DeploymentID": id_} + return self.request("pause", id_, json=pause_json, method="post").json() + + def promote_deployment_all(self, id_, _all=True): + """This endpoint is used to promote task groups that have canaries for a deployment. This should be done when + the placed canaries are healthy and the rolling upgrade of the remaining allocations should begin. + + https://www.nomadproject.io/docs/http/deployments.html + + arguments: + - id_ + - _all, Specifies whether all task groups should be promoted. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - promote_all_json = {"All": all, - "DeploymentID": id} - return self.request("promote", id, json=promote_all_json, method="post").json() - - def promote_deployment_groups(self, id, groups=list()): - """ This endpoint is used to promote task groups that have canaries for a deployment. This should be done when - the placed canaries are healthy and the rolling upgrade of the remaining allocations should begin. - - https://www.nomadproject.io/docs/http/deployments.html - - arguments: - - id - - groups, (list) Specifies a particular set of task groups that should be promoted - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + promote_all_json = {"All": _all, "DeploymentID": id_} + return self.request("promote", id_, json=promote_all_json, method="post").json() + + def promote_deployment_groups(self, id_, groups=None): + """This endpoint is used to promote task groups that have canaries for a deployment. This should be done when + the placed canaries are healthy and the rolling upgrade of the remaining allocations should begin. + + https://www.nomadproject.io/docs/http/deployments.html + + arguments: + - id_ + - groups, (list) Specifies a particular set of task groups that should be promoted + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - promote_groups_json = {"Groups": groups, - "DeploymentID": id} - return self.request("promote", id, json=promote_groups_json, method="post").json() - - def deployment_allocation_health(self, id, healthy_allocations=list(), unhealthy_allocations=list()): - """ This endpoint is used to set the health of an allocation that is in the deployment manually. In some use - cases, automatic detection of allocation health may not be desired. As such those task groups can be marked - with an upgrade policy that uses health_check = "manual". Those allocations must have their health marked - manually using this endpoint. Marking an allocation as healthy will allow the rolling upgrade to proceed. - Marking it as failed will cause the deployment to fail. - - https://www.nomadproject.io/docs/http/deployments.html - - arguments: - - id - - healthy_allocations, Specifies the set of allocation that should be marked as healthy. - - unhealthy_allocations, Specifies the set of allocation that should be marked as unhealthy. - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + if groups is None: + groups = [] + promote_groups_json = {"Groups": groups, "DeploymentID": id_} + return self.request("promote", id_, json=promote_groups_json, method="post").json() + + def deployment_allocation_health(self, id_, healthy_allocations=None, unhealthy_allocations=None): + """This endpoint is used to set the health of an allocation that is in the deployment manually. In some use + cases, automatic detection of allocation health may not be desired. As such those task groups can be marked + with an upgrade policy that uses health_check = "manual". Those allocations must have their health marked + manually using this endpoint. Marking an allocation as healthy will allow the rolling upgrade to proceed. + Marking it as failed will cause the deployment to fail. + + https://www.nomadproject.io/docs/http/deployments.html + + arguments: + - id_ + - healthy_allocations, Specifies the set of allocation that should be marked as healthy. + - unhealthy_allocations, Specifies the set of allocation that should be marked as unhealthy. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - allocations = {"HealthyAllocationIDs": healthy_allocations, - "UnHealthyAllocationIDs": unhealthy_allocations, - "DeploymentID": id} - return self.request("allocation-health", id, json=allocations, method="post").json() + if healthy_allocations is None: + healthy_allocations = [] + + if unhealthy_allocations is None: + unhealthy_allocations = [] + + allocations = { + "HealthyAllocationIDs": healthy_allocations, + "UnHealthyAllocationIDs": unhealthy_allocations, + "DeploymentID": id_, + } + return self.request("allocation-health", id_, json=allocations, method="post").json() diff --git a/nomad/api/deployments.py b/nomad/api/deployments.py index b840758..54d1012 100644 --- a/nomad/api/deployments.py +++ b/nomad/api/deployments.py @@ -1,25 +1,27 @@ +"""Nomad Deployment: https://developer.hashicorp.com/nomad/api-docs/deployments""" + import nomad.api.exceptions from nomad.api.base import Requester class Deployments(Requester): - """ The /deployment endpoints are used to query for and interact with deployments. https://www.nomadproject.io/docs/http/deployments.html """ + ENDPOINT = "deployments" def __init__(self, **kwargs): - super(Deployments, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): raise AttributeError @@ -35,40 +37,42 @@ def __iter__(self): def __contains__(self, item): try: deployments = self.get_deployments() - - for d in deployments: - if d["ID"] == item: + for deployment in deployments: + if deployment["ID"] == item: return True - else: - return False + + return False except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): try: deployments = self.get_deployments() - - for d in deployments: - if d["ID"] == item: - return d - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for deployment in deployments: + if deployment["ID"] == item: + return deployment raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc - def get_deployments(self, prefix=""): - """ This endpoint lists all deployments. + def get_deployments(self, prefix="", namespace=None): + """This endpoint lists all deployments. - https://www.nomadproject.io/docs/http/deployments.html + https://www.nomadproject.io/docs/http/deployments.html - optional_arguments: - - prefix, (default "") Specifies a string to filter deployments on based on an index prefix. - This is specified as a querystring parameter. + optional_arguments: + - prefix, (default "") Specifies a string to filter deployments on based on an index prefix. + This is specified as a querystring parameter. + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. - returns: list of dicts - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"prefix": prefix} + if namespace: + params["namespace"] = namespace + return self.request(params=params, method="get").json() diff --git a/nomad/api/evaluation.py b/nomad/api/evaluation.py index 2aa6bb2..6f8fcad 100644 --- a/nomad/api/evaluation.py +++ b/nomad/api/evaluation.py @@ -1,10 +1,11 @@ +"""Nomad Evaluation: https://developer.hashicorp.com/nomad/api-docs/evaluations""" + import nomad.api.exceptions from nomad.api.base import Requester class Evaluation(Requester): - """ The evaluation endpoint is used to query a specific evaluations. By default, the agent's local region is used; another region can @@ -12,63 +13,62 @@ class Evaluation(Requester): https://www.nomadproject.io/docs/http/eval.html """ + ENDPOINT = "evaluation" def __init__(self, **kwargs): - super(Evaluation, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): - try: - e = self.get_evaluation(item) + self.get_evaluation(item) return True except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): - try: - e = self.get_evaluation(item) - - if e["ID"] == item: - return e - except nomad.api.exceptions.URLNotFoundNomadException: + evaluation = self.get_evaluation(item) + if evaluation["ID"] == item: + return evaluation raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc - def get_evaluation(self, id): - """ Query a specific evaluation. + def get_evaluation(self, id_): + """Query a specific evaluation. - https://www.nomadproject.io/docs/http/eval.html + https://www.nomadproject.io/docs/http/eval.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + return self.request(id_, method="get").json() - def get_allocations(self, id): - """ Query the allocations created or modified by an evaluation. + def get_allocations(self, id_): + """Query the allocations created or modified by an evaluation. - https://www.nomadproject.io/docs/http/eval.html + https://www.nomadproject.io/docs/http/eval.html - arguments: - - id - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "allocations", method="get").json() + return self.request(id_, "allocations", method="get").json() diff --git a/nomad/api/evaluations.py b/nomad/api/evaluations.py index 8e581bc..f91c9d2 100644 --- a/nomad/api/evaluations.py +++ b/nomad/api/evaluations.py @@ -1,9 +1,11 @@ +"""Nomad Evaluations: https://developer.hashicorp.com/nomad/api-docs/evaluations""" + import nomad.api.exceptions from nomad.api.base import Requester -class Evaluations(Requester): +class Evaluations(Requester): """ The evaluations endpoint is used to query the status of evaluations. By default, the agent's local region is used; another region can @@ -11,16 +13,17 @@ class Evaluations(Requester): https://www.nomadproject.io/docs/http/evals.html """ + ENDPOINT = "evaluations" def __init__(self, **kwargs): - super(Evaluations, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): raise AttributeError @@ -29,11 +32,11 @@ def __contains__(self, item): try: evaluations = self.get_evaluations() - for e in evaluations: - if e["ID"] == item: + for evaluation in evaluations: + if evaluation["ID"] == item: return True - else: - return False + + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -45,29 +48,28 @@ def __getitem__(self, item): try: evaluations = self.get_evaluations() - for e in evaluations: - if e["ID"] == item: - return e - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for evaluation in evaluations: + if evaluation["ID"] == item: + return evaluation raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): evaluations = self.get_evaluations() return iter(evaluations) def get_evaluations(self, prefix=None): - """ Lists all the evaluations. - - https://www.nomadproject.io/docs/http/evals.html - arguments: - - prefix :(str) optional, specifies a string to filter evaluations on based on an prefix. - This is specified as a querystring parameter. - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + """Lists all the evaluations. + + https://www.nomadproject.io/docs/http/evals.html + arguments: + - prefix :(str) optional, specifies a string to filter evaluations on based on an prefix. + This is specified as a querystring parameter. + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"prefix": prefix} return self.request(method="get", params=params).json() diff --git a/nomad/api/event.py b/nomad/api/event.py new file mode 100644 index 0000000..e812b42 --- /dev/null +++ b/nomad/api/event.py @@ -0,0 +1,133 @@ +"""Nomad Events: https://developer.hashicorp.com/nomad/api-docs/events""" + +import json +import threading +import queue + +import requests + +from nomad.api.base import Requester + + +class Event: + """ + Nomad Event + """ + + def __str__(self): + return f"{self.__dict__}" + + def __repr__(self): + return f"{self.__dict__}" + + def __getattr__(self, item): + raise AttributeError + + def __init__(self, **kwargs): + self.stream = stream(**kwargs) + + +# backward compatibility +class stream(Requester): # pylint: disable=invalid-name + """ + The /event/stream endpoint is used to stream events generated by Nomad. + + https://www.nomadproject.io/api-docs/events + """ + + ENDPOINT = "event/stream" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def _get_stream(self, method, params, timeout, event_queue, exit_event): # pylint: disable=too-many-arguments + """ + Used as threading target, to obtain json() value + Args: + method: + params: + timeout: + event_queue: + exit_event: + """ + + while exit_event.is_set() is False: + try: + with self.request(method=method, params=params, timeout=timeout, stream=True) as resp: + for raw_msg in resp.iter_lines(): + msg = json.loads(raw_msg) + + # don't send heartbeats + if msg: + event_queue.put(msg) + + if exit_event.is_set(): + return + + except requests.exceptions.ConnectionError: + continue + + def get_stream( + self, index=0, topic=None, namespace=None, event_queue=None, timeout=None + ): # pylint: disable=too-many-arguments + """ + Usage: + stream, stream_exit_event, events = n.event.stream.get_stream() + stream.start() + + while True: + event = events.get() + print(event) + events.task_done() + + Args: + index: (int), Specifies the index to start streaming events from. If the requested index is no longer + in the buffer the stream will start at the next available index. + + topic: (None or dict), Specifies a topic to subscribe to and filter on. + The default is to subscribe to all topics. + Multiple topics may be specified by passing multiple topic parameters. + A valid topic parameter includes a topic type and an optional filter_key separated by a colon :. + As an example ?topic=Deployment:redis would subscribe to all Deployment events for a job redis. + an additional topic &topic=Deployment:web would include deployment events for redis and web. + To only subscribe to Node events a topic parameter of ?topic=Node without a separator value + would be used. ?topic=Node:* is also valid. + + namespace: (str) Specifies the target namespace to filter on. Specifying * includes all namespaces + for event types that support namespaces. + + event_queue: (None or queue.Queue) for thread listener to push events onto. + + timeout: (None or int), override timeout (seconds) so connection is not closed. + Defaults to timeout in constructor if not given. + + Returns: (threading.Thread), (threading.Event) (queue.Queue) + """ + + params = { + "index": index, + } + + if namespace: + params["namespace"] = namespace + + if topic: + params["topic"] = topic + + if event_queue is None: + event_queue = queue.Queue() + + stream_exit_event = threading.Event() + _stream = threading.Thread( + name="python-nomad-event-stream", + target=self._get_stream, + kwargs={ + "method": "get", + "params": params, + "timeout": timeout, + "event_queue": event_queue, + "exit_event": stream_exit_event, + }, + ) + + return _stream, stream_exit_event, event_queue diff --git a/nomad/api/exceptions.py b/nomad/api/exceptions.py index 685f4c7..dcc0d66 100644 --- a/nomad/api/exceptions.py +++ b/nomad/api/exceptions.py @@ -1,26 +1,40 @@ +"""Internal library exceptions""" + +import requests + + class BaseNomadException(Exception): """General Error occurred when interacting with nomad API""" + def __init__(self, nomad_resp): self.nomad_resp = nomad_resp + def __str__(self): + if isinstance(self.nomad_resp, requests.Response) and hasattr(self.nomad_resp, "text"): + return f"The {self.__class__.__name__} was raised with following response: {self.nomad_resp.text}." + + return f"The {self.__class__.__name__} was raised due to the following error: {self.nomad_resp}" + class URLNotFoundNomadException(BaseNomadException): """The requeted URL given does not exist""" - def __init__(self, nomad_resp): - self.nomad_resp = nomad_resp - + class URLNotAuthorizedNomadException(BaseNomadException): """The requested URL is not authorized. ACL""" - def __init__(self, nomad_resp): - self.nomad_resp = nomad_resp class BadRequestNomadException(BaseNomadException): """Validation failure and if a parameter is modified in the request, it could potentially succeed.""" - def __init__(self, nomad_resp): - self.nomad_resp = nomad_resp + + +class VariableConflict(BaseNomadException): + """In the case of a compare-and-set variable conflict""" class InvalidParameters(Exception): """Invalid parameters given""" + + +class TimeoutNomadException(requests.exceptions.RequestException): + """Timeout on request, if using a stream and timeout in conjunction""" diff --git a/nomad/api/job.py b/nomad/api/job.py index f3ee0f7..5ba2502 100644 --- a/nomad/api/job.py +++ b/nomad/api/job.py @@ -1,297 +1,363 @@ -import nomad.api.exceptions +"""Nomad job: https://developer.hashicorp.com/nomad/api-docs/jobs""" + +from typing import Union from nomad.api.base import Requester +import nomad.api.exceptions class Job(Requester): - """ The job endpoint is used for CRUD on a single job. By default, the agent's local region is used. https://www.nomadproject.io/docs/http/job.html """ + ENDPOINT = "job" def __init__(self, **kwargs): - super(Job, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): - try: - j = self.get_job(item) + self.get_job(item) return True except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): - try: - j = self.get_job(item) - - if j["ID"] == item: - return j - if j["Name"] == item: - return j - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: - raise KeyError + job = self.get_job(item) + if job["ID"] == item: + return job + if job["Name"] == item: + return job - def get_job(self, id): - """ Query a single job for its specification and status. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc + + def get_job(self, id_, namespace=None): + """Query a single job for its specification and status. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + params = {} - def get_versions(self, id): - """ This endpoint reads information about all versions of a job. + if namespace: + params["namespace"] = namespace - https://www.nomadproject.io/docs/http/job.html + return self.request(id_, method="get", params=params).json() - arguments: - - id - returns: list of dicts - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException - """ - return self.request(id, "versions", method="get").json() - - def get_allocations(self, id): - """ Query the allocations belonging to a single job. + def get_versions(self, id_): + """This endpoint reads information about all versions of a job. - https://www.nomadproject.io/docs/http/job.html + https://www.nomadproject.io/docs/http/job.html - arguments: - - id - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "allocations", method="get").json() - - def get_evaluations(self, id): - """ Query the evaluations belonging to a single job. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + return self.request(id_, "versions", method="get").json() + + def get_allocations( + self, + id_: str, + all_: Union[bool, None] = None, + namespace: Union[str, None] = None, + ): + """Query the allocations belonging to a single job. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - all (bool optional) + - namespace (str) optional. + Specifies the target namespace. If ACL is enabled, this value + must match a namespace that the token is allowed to access. + This is specified as a query string parameter. + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "evaluations", method="get").json() - - def get_deployments(self, id): - """ This endpoint lists a single job's deployments - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + params = { + "all": all_, + "namespace": namespace, + } + return self.request(id_, "allocations", params=params, method="get").json() + + def get_evaluations( + self, + id_: str, + namespace: Union[str, None] = None, + ): + """Query the evaluations belonging to a single job. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - namespace (str) optional. + Specifies the target namespace. If ACL is enabled, this value + must match a namespace that the token is allowed to access. + This is specified as a query string parameter. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + params = { + "namespace": namespace, + } + return self.request(id_, "evaluations", params=params, method="get").json() + + def get_deployments(self, id_): + """This endpoint lists a single job's deployments + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "deployments", method="get").json() + return self.request(id_, "deployments", method="get").json() - def get_deployment(self, id): - """ This endpoint returns a single job's most recent deployment. + def get_deployment(self, id_): + """This endpoint returns a single job's most recent deployment. - https://www.nomadproject.io/docs/http/job.html + https://www.nomadproject.io/docs/http/job.html - arguments: - - id - returns: list of dicts - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "deployment", method="get").json() + return self.request(id_, "deployment", method="get").json() - def get_summary(self, id): - """ Query the summary of a job. + def get_summary(self, id_): + """Query the summary of a job. - https://www.nomadproject.io/docs/http/job.html + https://www.nomadproject.io/docs/http/job.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "summary", method="get").json() + return self.request(id_, "summary", method="get").json() - def register_job(self, id, job): - """ Registers a new job or updates an existing job + def register_job(self, id_, job): + """Registers a new job or updates an existing job - https://www.nomadproject.io/docs/http/job.html + https://www.nomadproject.io/docs/http/job.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, json=job, method="post").json() + return self.request(id_, json=job, method="post").json() - def evaluate_job(self, id): - """ Creates a new evaluation for the given job. - This can be used to force run the scheduling logic if necessary. + def evaluate_job(self, id_): + """Creates a new evaluation for the given job. + This can be used to force run the scheduling logic if necessary. - https://www.nomadproject.io/docs/http/job.html + https://www.nomadproject.io/docs/http/job.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "evaluate", method="post").json() - - def plan_job(self, id, job, diff=False, policy_override=False): - """ Invoke a dry-run of the scheduler for the job. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - - job, dict - - diff, boolean - - policy_override, boolean - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + return self.request(id_, "evaluate", method="post").json() + + def plan_job(self, id_, job, diff=False, policy_override=False): + """Invoke a dry-run of the scheduler for the job. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - job, dict + - diff, boolean + - policy_override, boolean + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ json_dict = {} json_dict.update(job) - json_dict.setdefault('Diff', diff) - json_dict.setdefault('PolicyOverride', policy_override) - return self.request(id, "plan", json=json_dict, method="post").json() - - def periodic_job(self, id): - """ Forces a new instance of the periodic job. A new instance will be - created even if it violates the job's prohibit_overlap settings. - As such, this should be only used to immediately - run a periodic job. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + json_dict.setdefault("Diff", diff) + json_dict.setdefault("PolicyOverride", policy_override) + return self.request(id_, "plan", json=json_dict, method="post").json() + + def periodic_job(self, id_): + """Forces a new instance of the periodic job. A new instance will be + created even if it violates the job's prohibit_overlap settings. + As such, this should be only used to immediately + run a periodic job. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "periodic", "force", method="post").json() - - def dispatch_job(self, id, payload=None, meta=None): - """ Dispatches a new instance of a parameterized job. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - - payload - - meta - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + return self.request(id_, "periodic", "force", method="post").json() + + def dispatch_job( + self, + id_, + payload=None, + meta=None, + id_prefix_template=None, + idempotency_token=None, + ): # pylint: disable=too-many-arguments + """Dispatches a new instance of a parameterized job. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - payload + - meta + - id_prefix_template + - idempotency_token + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - dispatch_json = {"Meta": meta, "Payload": payload} - return self.request(id, "dispatch", json=dispatch_json, method="post").json() - - def revert_job(self, id, version, enforce_prior_version=None): - """ This endpoint reverts the job to an older version. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - - version, Specifies the job version to revert to. - optional_arguments: - - enforce_prior_version, Optional value specifying the current job's version. - This is checked and acts as a check-and-set value before reverting to the - specified job. - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + dispatch_json = { + "Meta": meta, + "Payload": payload, + "idempotency_token": idempotency_token, + "IdPrefixTemplate": id_prefix_template, + } + return self.request(id_, "dispatch", json=dispatch_json, method="post").json() + + def revert_job(self, id_, version, enforce_prior_version=None): + """This endpoint reverts the job to an older version. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - version, Specifies the job version to revert to. + optional_arguments: + - enforce_prior_version, Optional value specifying the current job's version. + This is checked and acts as a check-and-set value before reverting to the + specified job. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - revert_json = {"JobID": id, - "JobVersion": version, - "EnforcePriorVersion": enforce_prior_version} - return self.request(id, "revert", json=revert_json, method="post").json() - - def stable_job(self, id, version, stable): - """ This endpoint sets the job's stability. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - - version, Specifies the job version to revert to. - - stable, Specifies whether the job should be marked as stable or not. - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + revert_json = { + "JobID": id_, + "JobVersion": version, + "EnforcePriorVersion": enforce_prior_version, + } + return self.request(id_, "revert", json=revert_json, method="post").json() + + def stable_job(self, id_, version, stable): + """This endpoint sets the job's stability. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - version, Specifies the job version to revert to. + - stable, Specifies whether the job should be marked as stable or not. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - revert_json = {"JobID": id, - "JobVersion": version, - "Stable": stable} - return self.request(id, "stable", json=revert_json, method="post").json() - - def deregister_job(self, id, purge=None): - """ Deregisters a job, and stops all allocations part of it. - - https://www.nomadproject.io/docs/http/job.html - - arguments: - - id - - purge (bool), optionally specifies whether the job should be - stopped and purged immediately (`purge=True`) or deferred to the - Nomad garbage collector (`purge=False`). - - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException - - nomad.api.exceptions.InvalidParameters + revert_json = {"JobID": id_, "JobVersion": version, "Stable": stable} + return self.request(id_, "stable", json=revert_json, method="post").json() + + def deregister_job( + self, + id_: str, + eval_priority: Union[int, None] = None, + global_: Union[bool, None] = None, + namespace: Union[str, None] = None, + purge: Union[bool, None] = None, + ): # pylint: disable=too-many-arguments + """Deregisters a job, and stops all allocations part of it. + + https://www.nomadproject.io/docs/http/job.html + + arguments: + - id_ + - eval_priority (int) optional. + Override the priority of the evaluations produced as a result + of this job deregistration. By default, this is set to the + priority of the job. + - global_ (bool) optional. + Stop a multi-region job in all its regions. By default, job + stop will stop only a single region at a time. Ignored for + single-region jobs. + - purge (bool) optional. + Specifies that the job should be stopped and purged immediately. + This means the job will not be queryable after being stopped. + If not set, the job will be purged by the garbage collector. + - namespace (str) optional. + Specifies the target namespace. If ACL is enabled, this value + must match a namespace that the token is allowed to access. + This is specified as a query string parameter. + + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + - nomad.api.exceptions.InvalidParameters """ - params = None - if purge is not None: - if not isinstance(purge, bool): - raise nomad.api.exceptions.InvalidParameters("purge is invalid " - "(expected type %s but got %s)"%(type(bool()), type(purge))) - params = {"purge": purge} - return self.request(id, params=params, method="delete").json() + params = { + "eval_priority": eval_priority, + "global": global_, + "namespace": namespace, + "purge": purge, + } + return self.request(id_, params=params, method="delete").json() diff --git a/nomad/api/jobs.py b/nomad/api/jobs.py index c3abb03..1b3488c 100644 --- a/nomad/api/jobs.py +++ b/nomad/api/jobs.py @@ -1,10 +1,12 @@ +"""Nomad job: https://developer.hashicorp.com/nomad/api-docs/jobs""" + +from typing import Optional import nomad.api.exceptions from nomad.api.base import Requester class Jobs(Requester): - """ The jobs endpoint is used to query the status of existing jobs in Nomad and to register new jobs. @@ -12,19 +14,20 @@ class Jobs(Requester): https://www.nomadproject.io/docs/http/jobs.html """ + ENDPOINT = "jobs" def __init__(self, **kwargs): - super(Jobs, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return "{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return "{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): @@ -36,8 +39,7 @@ def __contains__(self, item): return True if j["Name"] == item: return True - else: - return False + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -54,51 +56,72 @@ def __getitem__(self, item): return j if j["Name"] == item: return j - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): jobs = self.get_jobs() return iter(jobs) - def get_jobs(self, prefix=None): - """ Lists all the jobs registered with Nomad. - - https://www.nomadproject.io/docs/http/jobs.html - arguments: - - prefix :(str) optional, specifies a string to filter jobs on based on an prefix. - This is specified as a querystring parameter. - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + def get_jobs( + self, + prefix: Optional[str] = None, + namespace: Optional[str] = None, + filter_: Optional[str] = None, + meta: Optional[bool] = None, + ): + """Lists all the jobs registered with Nomad. + + https://www.nomadproject.io/docs/http/jobs.html + arguments: + - prefix :(str) optional, specifies a string to filter jobs on based on an prefix. + This is specified as a querystring parameter. + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. + - filter_ :(str) optional, specifies the expression used to filter the result. + Name has a trailing underscore not to conflict with builtin function. + - meta :(bool) optional, if set, jobs returned will include a meta field containing + key-value pairs provided in the job specification's meta block. + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - params = {"prefix": prefix} + params = { + "prefix": prefix, + "namespace": namespace, + "filter": filter_, + "meta": meta, + } return self.request(method="get", params=params).json() def register_job(self, job): - """ Register a job with Nomad. + """Register a job with Nomad. - https://www.nomadproject.io/docs/http/jobs.html + https://www.nomadproject.io/docs/http/jobs.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(json=job, method="post").json() def parse(self, hcl, canonicalize=False): - """ Parse a HCL Job file. Returns a dict with the JSON formatted job. - This API endpoint is only supported from Nomad version 0.8.3. + """Parse a HCL Job file. Returns a dict with the JSON formatted job. + This API endpoint is only supported from Nomad version 0.8.3. - https://www.nomadproject.io/api/jobs.html#parse-job + https://www.nomadproject.io/api/jobs.html#parse-job - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("parse", json={"JobHCL": hcl, "Canonicalize": canonicalize}, method="post", allow_redirects=True).json() + return self.request( + "parse", + json={"JobHCL": hcl, "Canonicalize": canonicalize}, + method="post", + allow_redirects=True, + ).json() diff --git a/nomad/api/metrics.py b/nomad/api/metrics.py index beef3c3..acc5fab 100644 --- a/nomad/api/metrics.py +++ b/nomad/api/metrics.py @@ -1,8 +1,9 @@ +"""Nomad Metrics: https://developer.hashicorp.com/nomad/api-docs/metrics""" + from nomad.api.base import Requester class Metrics(Requester): - """ The /metrics endpoint returns metrics for the current Nomad process. @@ -12,28 +13,30 @@ class Metrics(Requester): https://www.nomadproject.io/docs/agent/telemetry.html """ + ENDPOINT = "metrics" def __init__(self, **kwargs): - super(Metrics, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def get_metrics(self): - """ Get the metrics + """Get the metrics - https://www.nomadproject.io/api/metrics.html + https://www.nomadproject.io/api/metrics.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(method="get").json() diff --git a/nomad/api/namespace.py b/nomad/api/namespace.py index 0f51b01..66f096c 100644 --- a/nomad/api/namespace.py +++ b/nomad/api/namespace.py @@ -1,107 +1,107 @@ +"""Nomad namespace: https://developer.hashicorp.com/nomad/api-docs/namespaces""" + import nomad.api.exceptions from nomad.api.base import Requester class Namespace(Requester): - """ The job endpoint is used for CRUD on a single namespace. By default, the agent's local region is used. https://www.nomadproject.io/api/namespaces.html """ + ENDPOINT = "namespace" def __init__(self, **kwargs): - super(Namespace, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): - try: - j = self.get_namespace(item) + self.get_namespace(item) return True except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): - try: - j = self.get_namespace(item) - - if j["ID"] == item: - return j - if j["Name"] == item: - return j - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + job = self.get_namespace(item) + + if job["ID"] == item: + return job + if job["Name"] == item: + return job + raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc - def get_namespace(self, id): - """ Query a single namespace. + def get_namespace(self, id_): + """Query a single namespace. - https://www.nomadproject.io/api/namespaces.html + https://www.nomadproject.io/api/namespaces.html - arguments: - - id - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + return self.request(id_, method="get").json() def create_namespace(self, namespace): - """ create namespace + """create namespace - https://www.nomadproject.io/api/namespaces.html + https://www.nomadproject.io/api/namespaces.html - arguments: - - id - - namespace (dict) - returns: requests.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id + - namespace (dict) + returns: requests.Response + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(json=namespace, method="post") - def update_namespace(self, id, namespace): - """ Update namespace + def update_namespace(self, id_, namespace): + """Update namespace - https://www.nomadproject.io/api/namespaces.html + https://www.nomadproject.io/api/namespaces.html - arguments: - - id - - namespace (dict) - returns: requests.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + - namespace (dict) + returns: requests.Response + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, json=namespace, method="post") + return self.request(id_, json=namespace, method="post") - def delete_namespace(self, id): - """ delete namespace. + def delete_namespace(self, id_): + """delete namespace. - https://www.nomadproject.io/api/namespaces.html + https://www.nomadproject.io/api/namespaces.html - arguments: - - id - returns: requests.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ + returns: requests.Response + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="delete") + return self.request(id_, method="delete") diff --git a/nomad/api/namespaces.py b/nomad/api/namespaces.py index 9483074..6017016 100644 --- a/nomad/api/namespaces.py +++ b/nomad/api/namespaces.py @@ -1,39 +1,41 @@ +"""Nomad namespace: https://developer.hashicorp.com/nomad/api-docs/namespaces""" + import nomad.api.exceptions from nomad.api.base import Requester class Namespaces(Requester): - """ The namespaces from enterprise solution https://www.nomadproject.io/docs/enterprise/namespaces/index.html """ + ENDPOINT = "namespaces" def __init__(self, **kwargs): - super(Namespaces, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): try: namespaces = self.get_namespaces() - for n in namespaces: - if n["Name"] == item: + for namespace in namespaces: + if namespace["Name"] == item: return True - else: - return False + + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -45,29 +47,29 @@ def __getitem__(self, item): try: namespaces = self.get_namespaces() - for n in namespaces: - if n["Name"] == item: - return n - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for namespace in namespaces: + if namespace["Name"] == item: + return namespace + raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): namespaces = self.get_namespaces() return iter(namespaces) def get_namespaces(self, prefix=None): - """ Lists all the namespaces registered with Nomad. - - https://www.nomadproject.io/docs/enterprise/namespaces/index.html - arguments: - - prefix :(str) optional, specifies a string to filter namespaces on based on an prefix. - This is specified as a querystring parameter. - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + """Lists all the namespaces registered with Nomad. + + https://www.nomadproject.io/docs/enterprise/namespaces/index.html + arguments: + - prefix :(str) optional, specifies a string to filter namespaces on based on an prefix. + This is specified as a querystring parameter. + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"prefix": prefix} return self.request(method="get", params=params).json() diff --git a/nomad/api/node.py b/nomad/api/node.py index bdff1a7..6b4099c 100644 --- a/nomad/api/node.py +++ b/nomad/api/node.py @@ -1,167 +1,166 @@ +"""Nomad Node: https://developer.hashicorp.com/nomad/api-docs/nodes""" + +from typing import Optional import nomad.api.exceptions from nomad.api.base import Requester class Node(Requester): - """ The node endpoint is used to query the a specific client node. By default, the agent's local region is used. https://www.nomadproject.io/docs/http/node.html """ + ENDPOINT = "node" def __init__(self, **kwargs): - super(Node, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - msg = "{0} does not exist".format(item) + msg = f"{item} does not exist" raise AttributeError(msg) def __contains__(self, item): - try: - n = self.get_node(item) + self.get_node(item) return True except nomad.api.exceptions.URLNotFoundNomadException: return False def __getitem__(self, item): - try: - n = self.get_node(item) - - if n["ID"] == item: - return n - if n["Name"] == item: - return n - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + node = self.get_node(item) + + if node["ID"] == item: + return node + if node["Name"] == item: + return node + raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc - def get_node(self, id): - """ Query the status of a client node registered with Nomad. + def get_node(self, id_: str): + """Query the status of a client node registered with Nomad. - https://www.nomadproject.io/docs/http/node.html + https://www.nomadproject.io/docs/http/node.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, method="get").json() + return self.request(id_, method="get").json() - def get_allocations(self, id): - """ Query the allocations belonging to a single node. + def get_allocations(self, id_: str): + """Query the allocations belonging to a single node. - https://www.nomadproject.io/docs/http/node.html + https://www.nomadproject.io/docs/http/node.html - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "allocations", method="get").json() + return self.request(id_, "allocations", method="get").json() - def evaluate_node(self, id): - """ Creates a new evaluation for the given node. - This can be used to force run the - scheduling logic if necessary. + def evaluate_node(self, id_: str): + """Creates a new evaluation for the given node. + This can be used to force run the + scheduling logic if necessary. - https://www.nomadproject.io/docs/http/node.html + https://www.nomadproject.io/docs/http/node.html - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "evaluate", method="post").json() - - def drain_node(self, id, enable=False): - """ Toggle the drain mode of the node. - When enabled, no further allocations will be - assigned and existing allocations will be migrated. - - https://www.nomadproject.io/docs/http/node.html - - arguments: - - id (str uuid): node id - - enable (bool): enable node drain or not to enable node drain - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + return self.request(id_, "evaluate", method="post").json() + + def drain_node(self, id_, enable: bool = False): + """Toggle the drain mode of the node. + When enabled, no further allocations will be + assigned and existing allocations will be migrated. + + https://www.nomadproject.io/docs/http/node.html + + arguments: + - id_ (str uuid): node id + - enable (bool): enable node drain or not to enable node drain + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "drain", params={"enable": enable}, method="post").json() + return self.request(id_, "drain", params={"enable": enable}, method="post").json() - def drain_node_with_spec(self, id, drain_spec, mark_eligible=None): - """ This endpoint toggles the drain mode of the node. When draining is enabled, - no further allocations will be assigned to this node, and existing allocations - will be migrated to new nodes. + def drain_node_with_spec(self, id_, drain_spec: Optional[dict], mark_eligible: Optional[bool] = None): + """This endpoint toggles the drain mode of the node. When draining is enabled, + no further allocations will be assigned to this node, and existing allocations + will be migrated to new nodes. - If an empty dictionary is given as drain_spec this will disable/toggle the drain. + If an empty dictionary is given as drain_spec this will disable/toggle the drain. - https://www.nomadproject.io/docs/http/node.html + https://www.nomadproject.io/docs/http/node.html - arguments: - - id (str uuid): node id - - drain_spec (dict): https://www.nomadproject.io/api/nodes.html#drainspec - - mark_eligible (bool): https://www.nomadproject.io/api/nodes.html#markeligible - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ (str uuid): node id + - drain_spec (dict): https://www.nomadproject.io/api/nodes.html#drainspec + - mark_eligible (bool): https://www.nomadproject.io/api/nodes.html#markeligible + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ payload = {} if drain_spec and mark_eligible is not None: payload = { - "NodeID": id, + "NodeID": id_, "DrainSpec": drain_spec, - "MarkEligible": mark_eligible + "MarkEligible": mark_eligible, } elif drain_spec and mark_eligible is None: - payload = { - "NodeID": id, - "DrainSpec": drain_spec - } + payload = {"NodeID": id_, "DrainSpec": drain_spec} elif not drain_spec and mark_eligible is not None: - payload = { - "NodeID": id, - "DrainSpec": None, - "MarkEligible": mark_eligible - } + payload = {"NodeID": id_, "DrainSpec": None, "MarkEligible": mark_eligible} elif not drain_spec and mark_eligible is None: payload = { - "NodeID": id, + "NodeID": id_, "DrainSpec": None, } - return self.request(id, "drain", json=payload, method="post").json() + return self.request(id_, "drain", json=payload, method="post").json() - def eligible_node(self, id, eligible=None, ineligible=None): - """ Toggle the eligibility of the node. + def eligible_node( + self, + id_: str, + eligible: Optional[bool] = None, + ineligible: Optional[bool] = None, + ): + """Toggle the eligibility of the node. - https://www.nomadproject.io/docs/http/node.html + https://www.nomadproject.io/docs/http/node.html - arguments: - - id (str uuid): node id - - eligible (bool): Set to True to mark node eligible - - ineligible (bool): Set to True to mark node ineligible - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - id_ (str uuid): node id + - eligible (bool): Set to True to mark node eligible + - ineligible (bool): Set to True to mark node ineligible + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ payload = {} @@ -171,25 +170,24 @@ def eligible_node(self, id, eligible=None, ineligible=None): raise nomad.api.exceptions.InvalidParameters if eligible is not None and eligible: - payload = {"Eligibility": "eligible", "NodeID": id} + payload = {"Eligibility": "eligible", "NodeID": id_} elif eligible is not None and not eligible: - payload = {"Eligibility": "ineligible", "NodeID": id} + payload = {"Eligibility": "ineligible", "NodeID": id_} elif ineligible is not None: - payload = {"Eligibility": "ineligible", "NodeID": id} + payload = {"Eligibility": "ineligible", "NodeID": id_} elif ineligible is not None and not ineligible: - payload = {"Eligibility": "eligible", "NodeID": id} + payload = {"Eligibility": "eligible", "NodeID": id_} - return self.request(id, "eligibility", json=payload, method="post").json() + return self.request(id_, "eligibility", json=payload, method="post").json() - def purge_node(self, id): - """ This endpoint purges a node from the system. Nodes can still join the cluster if they are alive. + def purge_node(self, id_: str): + """This endpoint purges a node from the system. Nodes can still join the cluster if they are alive. arguments: - - id (str uuid): node id + - id_ (str uuid): node id returns: dict raises: - nomad.api.exceptions.BaseNomadException - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request(id, "purge", method="post").json() - + return self.request(id_, "purge", method="post").json() diff --git a/nomad/api/nodes.py b/nomad/api/nodes.py index 30dd1b0..654fde9 100644 --- a/nomad/api/nodes.py +++ b/nomad/api/nodes.py @@ -1,41 +1,45 @@ +"""Nomad Node: https://developer.hashicorp.com/nomad/api-docs/nodes""" + +from typing import Optional + import nomad.api.exceptions from nomad.api.base import Requester class Nodes(Requester): - """ The nodes endpoint is used to query the status of client nodes. By default, the agent's local region is used https://www.nomadproject.io/docs/http/nodes.html """ + ENDPOINT = "nodes" def __init__(self, **kwargs): - super(Nodes, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def __contains__(self, item): try: nodes = self.get_nodes() - for n in nodes: - if n["ID"] == item: + for node in nodes: + if node["ID"] == item: return True - if n["Name"] == item: + if node["Name"] == item: return True - else: - return False + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -47,31 +51,45 @@ def __getitem__(self, item): try: nodes = self.get_nodes() - for n in nodes: - if n["ID"] == item: - return n - if n["Name"] == item: - return n - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for node in nodes: + if node["ID"] == item: + return node + if node["Name"] == item: + return node raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): nodes = self.get_nodes() return iter(nodes) - def get_nodes(self, prefix=None): - """ Lists all the client nodes registered with Nomad. - - https://www.nomadproject.io/docs/http/nodes.html - arguments: - - prefix :(str) optional, specifies a string to filter nodes on based on an prefix. - This is specified as a querystring parameter. - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + def get_nodes( # pylint: disable=too-many-arguments + self, + prefix: Optional[str] = None, + next_token: Optional[str] = None, + per_page: Optional[str] = None, + filter_: Optional[str] = None, + resources: Optional[bool] = None, + os: Optional[bool] = None, # pylint: disable=invalid-name + ): + """Lists all the client nodes registered with Nomad. + + https://www.nomadproject.io/docs/http/nodes.html + arguments: + - prefix :(str) optional, specifies a string to filter nodes on based on an prefix. + This is specified as a querystring parameter. + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - params = {"prefix": prefix} + params = { + "prefix": prefix, + "next_token": next_token, + "per_page": per_page, + "filter": filter_, + "resources": resources, + "os": os, + } return self.request(method="get", params=params).json() diff --git a/nomad/api/operator.py b/nomad/api/operator.py index 52cef1f..777d5d8 100644 --- a/nomad/api/operator.py +++ b/nomad/api/operator.py @@ -1,8 +1,9 @@ +"""Nomad Operator: https://developer.hashicorp.com/nomad/api-docs/operator""" + from nomad.api.base import Requester class Operator(Requester): - """ The Operator endpoint provides cluster-level tools for Nomad operators, such as interacting with the Raft subsystem. @@ -13,49 +14,50 @@ class Operator(Requester): ENDPOINT = "operator" def __init__(self, **kwargs): - super(Operator, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def get_configuration(self, stale=False): - """ Query the status of a client node registered with Nomad. + """Query the status of a client node registered with Nomad. - https://www.nomadproject.io/docs/http/operator.html + https://www.nomadproject.io/docs/http/operator.html - returns: dict - optional arguments: - - stale, (defaults to False), Specifies if the cluster should respond without an active leader. - This is specified as a querystring parameter. - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: dict + optional arguments: + - stale, (defaults to False), Specifies if the cluster should respond without an active leader. + This is specified as a querystring parameter. + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"stale": stale} return self.request("raft", "configuration", params=params, method="get").json() def delete_peer(self, peer_address, stale=False): - """ Remove the Nomad server with given address from the Raft configuration. - The return code signifies success or failure. - - https://www.nomadproject.io/docs/http/operator.html - - arguments: - - peer_address, The address specifies the server to remove and is given as an IP:port - optional arguments: - - stale, (defaults to False), Specifies if the cluster should respond without an active leader. - This is specified as a querystring parameter. - returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + """Remove the Nomad server with given address from the Raft configuration. + The return code signifies success or failure. + + https://www.nomadproject.io/docs/http/operator.html + + arguments: + - peer_address, The address specifies the server to remove and is given as an IP:port + optional arguments: + - stale, (defaults to False), Specifies if the cluster should respond without an active leader. + This is specified as a querystring parameter. + returns: Boolean + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ params = {"address": peer_address, "stale": stale} diff --git a/nomad/api/regions.py b/nomad/api/regions.py index 49bc765..0b22a0d 100644 --- a/nomad/api/regions.py +++ b/nomad/api/regions.py @@ -1,36 +1,38 @@ +"""Nomad region: https://developer.hashicorp.com/nomad/api-docs/regions""" + import nomad.api.exceptions from nomad.api.base import Requester class Regions(Requester): - """ https://www.nomadproject.io/docs/http/regions.html """ + ENDPOINT = "regions" def __init__(self, **kwargs): - super(Regions, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def __contains__(self, item): try: regions = self.get_regions() - for r in regions: - if r == item: + for region in regions: + if region == item: return True - else: - return False + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -42,26 +44,25 @@ def __getitem__(self, item): try: regions = self.get_regions() - for r in regions: - if r == item: - return r - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for region in regions: + if region == item: + return region raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): regions = self.get_regions() return iter(regions) def get_regions(self): - """ Returns the known region names. + """Returns the known region names. - https://www.nomadproject.io/docs/http/regions.html + https://www.nomadproject.io/docs/http/regions.html - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(method="get").json() diff --git a/nomad/api/scaling.py b/nomad/api/scaling.py new file mode 100644 index 0000000..3adf426 --- /dev/null +++ b/nomad/api/scaling.py @@ -0,0 +1,74 @@ +"""Nomad Scalling API: https://developer.hashicorp.com/nomad/api-docs/scaling-policies""" + +import nomad.api.exceptions + +from nomad.api.base import Requester + + +class Scaling(Requester): + """ + Endpoints are used to list and view scaling policies. + + https://developer.hashicorp.com/nomad/api-docs/scaling-policies + """ + + ENDPOINT = "scaling" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __str__(self): + return f"{self.__dict__}" + + def __repr__(self): + return f"{self.__dict__}" + + def __getattr__(self, item): + msg = f"{item} does not exist" + raise AttributeError(msg) + + # we want to have common arguments name with Nomad API + def get_scaling_policies(self, job="", type_=""): # pylint: disable=redefined-builtin + """ + This endpoint returns the scaling policies from all jobs. + + https://developer.hashicorp.com/nomad/api-docs/scaling-policies#list-scaling-policies + + arguments: + - job + - type_ + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + type_of_scaling_policies = [ + "horizontal", + "vertical_mem", + "vertical_cpu", + "", + ] # we have only horizontal in OSS + + if type_ not in type_of_scaling_policies: + raise nomad.api.exceptions.InvalidParameters( + "type is invalid " f"(expected values are {type_of_scaling_policies} but got {type_})" + ) + + params = {"job": job, "type": type_} + + return self.request("policies", method="get", params=params).json() + + def get_scaling_policy(self, id_): + """ + This endpoint reads a specific scaling policy. + + https://developer.hashicorp.com/nomad/api-docs/scaling-policies#read-scaling-policy + + arguments: + - id_ + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + return self.request(f"policy/{id_}", method="get").json() diff --git a/nomad/api/search.py b/nomad/api/search.py new file mode 100644 index 0000000..e104e41 --- /dev/null +++ b/nomad/api/search.py @@ -0,0 +1,98 @@ +"""Nomad Search API: https://developer.hashicorp.com/nomad/api-docs/search""" + +import nomad.api.exceptions + +from nomad.api.base import Requester + + +class Search(Requester): + """ + The endpoint returns matches for a given prefix and context, where a context can be jobs, allocations, evaluations, + nodes, deployments, plugins, namespaces, or volumes. + When using Nomad Enterprise, the allowed contexts include quotas. + Additionally, a prefix can be searched for within every context. + + https://developer.hashicorp.com/nomad/api-docs/search + """ + + ENDPOINT = "search" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __str__(self): + return f"{self.__dict__}" + + def __repr__(self): + return f"{self.__dict__}" + + def __getattr__(self, item): + msg = f"{item} does not exist" + raise AttributeError(msg) + + def search(self, prefix, context): + """The endpoint returns matches for a given prefix and context, where a context can be jobs, + allocations, evaluations, nodes, deployments, plugins, namespaces, or volumes. + + https://developer.hashicorp.com/nomad/api-docs/search + arguments: + - prefix:(str) required, specifies the identifier against which matches will be found. + For example, if the given prefix were "a", potential matches might be "abcd", or "aabb". + - context:(str) defines the scope in which a search for a prefix operates. + Contexts can be: "jobs", "evals", "allocs", "nodes", "deployment", "plugins", + "volumes" or "all", where "all" means every context will be searched. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + - nomad.api.exceptions.InvalidParameters + """ + accetaple_contexts = ( + "jobs", + "evals", + "allocs", + "nodes", + "deployment", + "plugins", + "volumes", + "all", + ) + if context not in accetaple_contexts: + raise nomad.api.exceptions.InvalidParameters( + "context is invalid " f"(expected values are {accetaple_contexts} but got {context})" + ) + params = {"Prefix": prefix, "Context": context} + + return self.request(json=params, method="post").json() + + def fuzzy_search(self, text, context): + """The /search/fuzzy endpoint returns partial substring matches for a given search term and context, + where a context can be jobs, allocations, nodes, plugins, or namespaces. Additionally, + fuzzy searching can be done across all contexts. + + https://developer.hashicorp.com/nomad/api-docs/search#fuzzy-searching + arguments: + - text:(str) required, specifies the identifier against which matches will be found. + For example, if the given text were "py", potential fuzzy matches might be "python", "spying", + or "happy". + - context:(str) defines the scope in which a search for a prefix operates. Contexts can be: + "jobs", "allocs", "nodes", "plugins", or "all", where "all" means every context will + be searched. + When "all" is selected, additional prefix matches will be included for the "deployments", + "evals", and "volumes" types. When searching in the "jobs" context, results that fuzzy match + "groups", "services", "tasks", "images", "commands", and "classes" are also included in the results. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + + params = {"Text": text, "Context": context} + + accetaple_contexts = ("jobs", "allocs", "nodes", "plugins", "all") + if context not in accetaple_contexts: + raise nomad.api.exceptions.InvalidParameters( + "context is invalid " f"(expected values are {accetaple_contexts} but got {context})" + ) + + return self.request("fuzzy", json=params, method="post").json() diff --git a/nomad/api/sentinel.py b/nomad/api/sentinel.py index 29d91a4..ef2a0f0 100644 --- a/nomad/api/sentinel.py +++ b/nomad/api/sentinel.py @@ -1,8 +1,9 @@ +"""Nomad Sentinel API: https://developer.hashicorp.com/nomad/api-docs/sentinel-policies""" + from nomad.api.base import Requester class Sentinel(Requester): - """ The endpoint manage sentinel policies (Enterprise Only) @@ -12,85 +13,86 @@ class Sentinel(Requester): ENDPOINT = "sentinel" def __init__(self, **kwargs): - super(Sentinel, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def get_policies(self): - """ Get a list of policies. + """Get a list of policies. - https://www.nomadproject.io/api/sentinel-policies.html + https://www.nomadproject.io/api/sentinel-policies.html - returns: list + returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("policies", method="get").json() - def create_policy(self, id, policy): - """ Create policy. + def create_policy(self, id_, policy): + """Create policy. - https://www.nomadproject.io/api/sentinel-policies.html + https://www.nomadproject.io/api/sentinel-policies.html - arguments: - - policy - returns: requests.Response + arguments: + - policy + returns: requests.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, json=policy, method="post") + return self.request("policy", id_, json=policy, method="post") - def get_policy(self, id): - """ Get a spacific policy. + def get_policy(self, id_): + """Get a spacific policy. - https://www.nomadproject.io/api/sentinel-policies.html + https://www.nomadproject.io/api/sentinel-policies.html - returns: dict + returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, method="get").json() + return self.request("policy", id_, method="get").json() - def update_policy(self, id, policy): - """ Create policy. + def update_policy(self, id_, policy): + """Create policy. - https://www.nomadproject.io/api/sentinel-policies.html + https://www.nomadproject.io/api/sentinel-policies.html - arguments: - - name - - policy - returns: requests.Response + arguments: + - name + - policy + returns: requests.Response - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, json=policy, method="post") + return self.request("policy", id_, json=policy, method="post") - def delete_policy(self, id): - """ Delete specific policy. + def delete_policy(self, id_): + """Delete specific policy. - https://www.nomadproject.io/api/sentinel-policies.html + https://www.nomadproject.io/api/sentinel-policies.html - arguments: - - id - returns: Boolean + arguments: + - id_ + returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ - return self.request("policy", id, method="delete").ok + return self.request("policy", id_, method="delete").ok diff --git a/nomad/api/status.py b/nomad/api/status.py index ba598e0..db52510 100644 --- a/nomad/api/status.py +++ b/nomad/api/status.py @@ -1,10 +1,11 @@ +"""Nomad Status API: https://developer.hashicorp.com/nomad/api-docs/status""" + import nomad.api.exceptions from nomad.api.base import Requester -class Status(object): - +class Status: """ By default, the agent's local region is used @@ -16,16 +17,18 @@ def __init__(self, **kwargs): self.peers = Peers(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) class Leader(Requester): + """This endpoint returns the address of the current leader in the region.""" ENDPOINT = "status/leader" @@ -35,8 +38,8 @@ def __contains__(self, item): if leader == item: return True - else: - return False + + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -45,19 +48,20 @@ def __len__(self): return len(leader) def get_leader(self): - """ Returns the address of the current leader in the region. + """Returns the address of the current leader in the region. - https://www.nomadproject.io/docs/http/status.html + https://www.nomadproject.io/docs/http/status.html - returns: string - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: string + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(method="get").json() class Peers(Requester): + """This endpoint returns the set of raft peers in the region.""" ENDPOINT = "status/peers" @@ -65,11 +69,10 @@ def __contains__(self, item): try: peers = self.get_peers() - for p in peers: - if p == item: + for peer in peers: + if peer == item: return True - else: - return False + return False except nomad.api.exceptions.URLNotFoundNomadException: return False @@ -81,26 +84,25 @@ def __getitem__(self, item): try: peers = self.get_peers() - for p in peers: - if p == item: - return p - else: - raise KeyError - except nomad.api.exceptions.URLNotFoundNomadException: + for peer in peers: + if peer == item: + return peer raise KeyError + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc def __iter__(self): peers = self.get_peers() return iter(peers) def get_peers(self): - """ Returns the set of raft peers in the region. + """Returns the set of raft peers in the region. - https://www.nomadproject.io/docs/http/status.html + https://www.nomadproject.io/docs/http/status.html - returns: list - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: list + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request(method="get").json() diff --git a/nomad/api/system.py b/nomad/api/system.py index 923d587..daf119f 100644 --- a/nomad/api/system.py +++ b/nomad/api/system.py @@ -1,8 +1,9 @@ +"""Nomad System API: https://developer.hashicorp.com/nomad/api-docs/system""" + from nomad.api.base import Requester class System(Requester): - """ The system endpoint is used to for system maintenance and should not be necessary for most users. @@ -14,37 +15,38 @@ class System(Requester): ENDPOINT = "system" def __init__(self, **kwargs): - super(System, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def initiate_garbage_collection(self): - """ Initiate garbage collection of jobs, evals, allocations and nodes. + """Initiate garbage collection of jobs, evals, allocations and nodes. - https://www.nomadproject.io/docs/http/system.html + https://www.nomadproject.io/docs/http/system.html - returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: Boolean + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("gc", method="put").ok def reconcile_summaries(self): - """ This endpoint reconciles the summaries of all registered jobs. + """This endpoint reconciles the summaries of all registered jobs. - https://www.nomadproject.io/docs/http/system.html + https://www.nomadproject.io/docs/http/system.html - returns: Boolean - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + returns: Boolean + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("reconcile", "summaries", method="put").ok diff --git a/nomad/api/validate.py b/nomad/api/validate.py index a0c3d7d..5372905 100644 --- a/nomad/api/validate.py +++ b/nomad/api/validate.py @@ -1,8 +1,9 @@ +"""Nomad Validate API: https://developer.hashicorp.com/nomad/api-docs/validate""" + from nomad.api.base import Requester class Validate(Requester): - """ The system endpoint is used to for system maintenance and should not be necessary for most users. @@ -14,29 +15,30 @@ class Validate(Requester): ENDPOINT = "validate" def __init__(self, **kwargs): - super(Validate, self).__init__(**kwargs) + super().__init__(**kwargs) def __str__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __repr__(self): - return "{0}".format(self.__dict__) + return f"{self.__dict__}" def __getattr__(self, item): - raise AttributeError + msg = f"{item} does not exist" + raise AttributeError(msg) def validate_job(self, nomad_job_dict): - """ This endpoint validates a Nomad job file. The local Nomad agent forwards the request to a server. + """This endpoint validates a Nomad job file. The local Nomad agent forwards the request to a server. In the event a server can't be reached the agent verifies the job file locally but skips validating driver configurations. - https://www.nomadproject.io/api/validate.html + https://www.nomadproject.io/api/validate.html - arguments: - - nomad_job_json, any valid nomad job IN dict FORMAT - returns: dict - raises: - - nomad.api.exceptions.BaseNomadException - - nomad.api.exceptions.URLNotFoundNomadException + arguments: + - nomad_job_json, any valid nomad job IN dict FORMAT + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException """ return self.request("job", json=nomad_job_dict, method="post") diff --git a/nomad/api/variable.py b/nomad/api/variable.py new file mode 100644 index 0000000..e4bcdf1 --- /dev/null +++ b/nomad/api/variable.py @@ -0,0 +1,111 @@ +"""Nomad Valiables API: https://developer.hashicorp.com/nomad/api-docs/variables""" + +import nomad.api.exceptions + +from nomad.api.base import Requester + + +class Variable(Requester): + """ + The /var endpoints are used to read or create variables. + https://developer.hashicorp.com/nomad/api-docs/variables + """ + + ENDPOINT = "var" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __str__(self): + return f"{self.__dict__}" + + def __repr__(self): + return f"{self.__dict__}" + + def __getattr__(self, item): + msg = f"{item} does not exist" + raise AttributeError(msg) + + def __contains__(self, item): + try: + self.get_variable(item) + return True + except nomad.api.exceptions.URLNotFoundNomadException: + return False + + def __getitem__(self, item): + try: + return self.get_variable(item) + except nomad.api.exceptions.URLNotFoundNomadException as exc: + raise KeyError from exc + + def get_variable(self, var_path, namespace=None): + """ + This endpoint reads a specific variable by path. This API returns the decrypted variable body. + https://developer.hashicorp.com/nomad/api-docs/variables#read-variable + + arguments: + - var_path :(str), path to variable + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + params = {} + if namespace: + params["namespace"] = namespace + + return self.request(var_path, params=params, method="get").json() + + def create_variable(self, var_path, payload, namespace=None, cas=None): + """ + This endpoint creates or updates a variable. + https://developer.hashicorp.com/nomad/api-docs/variables#create-variable + + arguments: + - var_path :(str), path to variable + - payload :(dict), variable object. Example: + https://developer.hashicorp.com/nomad/api-docs/variables#sample-payload + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. + - cas :(int) optional, If set, the variable will only be deleted if the cas value matches the + current variables ModifyIndex. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + - nomad.api.exceptions.VariableConflict + """ + params = {} + if cas is not None: + params["cas"] = cas + if namespace: + params["namespace"] = namespace + + return self.request(var_path, params=params, json=payload, method="put").json() + + def delete_variable(self, var_path, namespace=None, cas=None): + """ + This endpoint reads a specific variable by path. This API returns the decrypted variable body. + https://developer.hashicorp.com/nomad/api-docs/variables#delete-variable + + arguments: + - var_path :(str), path to variable + - namespace :(str) optional, specifies the target namespace. Specifying * would return all jobs. + This is specified as a querystring parameter. + - cas :(int) optional, If set, the variable will only be deleted if the cas value matches the + current variables ModifyIndex. + returns: dict + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + - nomad.api.exceptions.VariableConflict + """ + params = {} + if cas is not None: + params["cas"] = cas + if namespace: + params["namespace"] = namespace + + # we need to return json here but because of bug we recieve empty response + return self.request(var_path, params=params, method="delete") diff --git a/nomad/api/variables.py b/nomad/api/variables.py new file mode 100644 index 0000000..2d9d972 --- /dev/null +++ b/nomad/api/variables.py @@ -0,0 +1,66 @@ +"""Nomad Valiables API: https://developer.hashicorp.com/nomad/api-docs/variables""" + +from nomad.api.base import Requester + + +class Variables(Requester): + """ + The /vars endpoints are used to query for and interact with variables. + https://developer.hashicorp.com/nomad/api-docs/variables + """ + + ENDPOINT = "vars" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __str__(self): + return f"{self.__dict__}" + + def __repr__(self): + return f"{self.__dict__}" + + def __getattr__(self, item): + msg = f"{item} does not exist" + raise AttributeError(msg) + + def __contains__(self, item): + variables = self.get_variables() + + for var in variables: + if var["Path"] == item: + return True + return False + + def __getitem__(self, item): + variables = self.get_variables() + + for var in variables: + if var["Path"] == item: + return var + raise KeyError + + def __iter__(self): + variables = self.get_variables() + return iter(variables) + + def get_variables(self, prefix="", namespace=None): + """ + This endpoint lists variables. + https://developer.hashicorp.com/nomad/api-docs/variables + + optional_arguments: + - prefix, (default "") Specifies a string to filter variables on based on an index prefix. + This is specified as a query string parameter. + - namespace :(str) optional, Specifies the target namespace. + Specifying * will return all variables across all the authorized namespaces. + returns: list of dicts + raises: + - nomad.api.exceptions.BaseNomadException + - nomad.api.exceptions.URLNotFoundNomadException + """ + params = {"prefix": prefix} + if namespace: + params["namespace"] = namespace + + return self.request(params=params, method="get").json() diff --git a/requirements-dev.txt b/requirements-dev.txt index 6dc78cd..6e3596d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ -coverage==4.1 -pytest==2.9.1 -pytest-cov==2.2.1 -mkdocs==0.15.3 -mock==1.2.0 -responses==0.9.0 -flaky==3.4.0 \ No newline at end of file +coverage==6.5.0 +pytest==7.2.0 +pytest-cov==4.0.0 +mkdocs==1.4.2 +mock==4.0.3 +flaky==3.7.0 +responses==0.22.0 \ No newline at end of file diff --git a/requirements-lint.txt b/requirements-lint.txt new file mode 100644 index 0000000..5d0c3b3 --- /dev/null +++ b/requirements-lint.txt @@ -0,0 +1,2 @@ +pylint==3.1.0 +black==24.4.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c20f36f..6e42168 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests==2.20.0 +requests==2.32.2 diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..eea22cf --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Run tests with local Nomad binaries. +set -ueo pipefail + +if [ "${1-}" == "init" ]; then + virtualenv .venv + pip install -r requirements-dev.txt + source .venv/bin/activate +fi + +NOMAD_VERSION=`nomad --version | awk 'NR==1{print $2}' | cut -c2-` + +echo "Run Nomad in dev mode" +nomad agent -dev -node pynomad1 --acl-enabled &> nomad.log & +NOMAD_PID=$! + +sleep 3 + +echo "Run tests with Nomad $NOMAD_VERSION" +NOMAD_IP=127.0.0.1 NOMAD_VERSION=$NOMAD_VERSION py.test -s --cov=nomad --cov-report=term-missing --runxfail tests/ || true + + +echo "Kill nomad in background" +kill ${NOMAD_PID} diff --git a/setup.py b/setup.py index 8a30cdd..c44d07b 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,12 @@ -from distutils.core import setup +"""Nomad Python Library""" +import setuptools -setup( +with open("README.md", "r", encoding='utf-8') as fh: + long_description = fh.read() + +setuptools.setup( name='python-nomad', - version='1.1.0', + version='2.1.0', install_requires=['requests'], packages=['nomad', 'nomad.api'], url='http://github.com/jrxfive/python-nomad', @@ -10,14 +14,19 @@ author='jrxfive', author_email='jrxfive@gmail.com', description='Client library for Hashicorp Nomad', + long_description=long_description, + long_description_content_type="text/markdown", classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], keywords='nomad hashicorp client', ) diff --git a/tests/conftest.py b/tests/conftest.py index a871f3b..8630b01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,16 @@ import pytest import tests.common as common + @pytest.fixture def nomad_setup(): n = nomad.Nomad(host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN) return n + @pytest.fixture def nomad_setup_with_namespace(): - n = nomad.Nomad(host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN, namespace=common.NOMAD_NAMESPACE) + n = nomad.Nomad( + host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN, namespace=common.NOMAD_NAMESPACE + ) return n diff --git a/tests/test_acl.py b/tests/test_acl.py index ec7c151..863959d 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -7,7 +7,10 @@ # integration tests requires nomad Vagrant VM or Binary running # IMPORTANT: without token activated -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=0) def test_create_bootstrap(nomad_setup): bootstrap = nomad_setup.acl.generate_bootstrap() @@ -15,45 +18,59 @@ def test_create_bootstrap(nomad_setup): common.NOMAD_TOKEN = bootstrap["SecretID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=1) def test_list_tokens(nomad_setup): assert "Bootstrap Token" in nomad_setup.acl.get_tokens()[0]["Name"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=2) def test_create_token(nomad_setup): - token_example='{"Name": "Readonly token","Type": "client","Policies": ["readonly"],"Global": false}' + token_example = '{"Name": "Readonly token","Type": "client","Policies": ["readonly"],"Global": false}' json_token = json.loads(token_example) created_token = nomad_setup.acl.create_token(json_token) assert "Readonly token" in created_token["Name"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=3) def test_list_all_tokens(nomad_setup): tokens = nomad_setup.acl.get_tokens() assert isinstance(tokens, list) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=4) def test_update_token(nomad_setup): - token_example='{"Name": "CreatedForUpdate","Type": "client","Policies": ["readonly"],"Global": false}' + token_example = '{"Name": "CreatedForUpdate","Type": "client","Policies": ["readonly"],"Global": false}' json_token = json.loads(token_example) created_token = nomad_setup.acl.create_token(json_token) - token_update ='{"AccessorID":"' + created_token["AccessorID"] + '","Name": "Updated" ,"Type": "client","Policies": ["readonly"]}' + token_update = ( + '{"AccessorID":"' + + created_token["AccessorID"] + + '","Name": "Updated" ,"Type": "client","Policies": ["readonly"]}' + ) json_token_update = json.loads(token_update) - update_token = nomad_setup.acl.update_token(id=created_token["AccessorID"],token=json_token_update) + update_token = nomad_setup.acl.update_token(id_=created_token["AccessorID"], token=json_token_update) assert "Updated" in update_token["Name"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=5) def test_get_token(nomad_setup): - token_example='{"Name": "GetToken","Type": "client","Policies": ["readonly"],"Global": false}' + token_example = '{"Name": "GetToken","Type": "client","Policies": ["readonly"],"Global": false}' json_token = json.loads(token_example) created_token = nomad_setup.acl.create_token(json_token) @@ -61,10 +78,12 @@ def test_get_token(nomad_setup): assert "GetToken" in created_token["Name"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) @pytest.mark.run(order=6) def test_delete_token(nomad_setup): - token_example='{"Name": "DeleteToken","Type": "client","Policies": ["readonly"],"Global": false}' + token_example = '{"Name": "DeleteToken","Type": "client","Policies": ["readonly"],"Global": false}' json_token = json.loads(token_example) created_token = nomad_setup.acl.create_token(json_token) assert "DeleteToken" in created_token["Name"] @@ -73,41 +92,53 @@ def test_delete_token(nomad_setup): assert False == any("DeleteToken" in x for x in nomad_setup.acl.get_tokens()) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_get_self_token(nomad_setup): current_token = nomad_setup.acl.get_self_token() assert nomad_setup.get_token() in current_token["SecretID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_get_policies(nomad_setup): policies = nomad_setup.acl.get_policies() assert isinstance(policies, list) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_create_policy(nomad_setup): policy_example = '{ "Name": "my-policy", "Description": "This is a great policy", "Rules": "" }' json_policy = json.loads(policy_example) - nomad_setup.acl.create_policy(id="my-policy", policy=json_policy) + nomad_setup.acl.create_policy(id_="my-policy", policy=json_policy) assert False == any("my-policy" in x for x in nomad_setup.acl.get_policies()) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_get_policy(nomad_setup): policy = nomad_setup.acl.get_policy("my-policy") assert "This is a great policy" in policy["Description"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_update_policy(nomad_setup): policy_update = '{"Name": "my-policy","Description": "Updated","Rules": ""}' json_policy_update = json.loads(policy_update) - nomad_setup.acl.update_policy(id="my-policy", policy=json_policy_update) + nomad_setup.acl.update_policy(id_="my-policy", policy=json_policy_update) assert False == any("Updated" in x for x in nomad_setup.acl.get_policies()) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_delete_policy(nomad_setup): - nomad_setup.acl.delete_policy(id="my-policy") + nomad_setup.acl.delete_policy(id_="my-policy") assert False == any("my-policy" in x for x in nomad_setup.acl.get_policies()) diff --git a/tests/test_agent.py b/tests/test_agent.py index 16c9138..dccceb3 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -29,21 +29,6 @@ def test_join_agent(nomad_setup): assert r["num_joined"] == 0 -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 4, 1), reason="Not supported in version") -def test_update_servers(nomad_setup): - known_servers = nomad_setup.agent.get_servers() - r = nomad_setup.agent.update_servers(known_servers) - assert r == 200 - assert known_servers[0] in nomad_setup.agent.get_servers() - - # 0.8 enforces list of known servers to the provided list releases below do allow this functionality - try: - nomad_setup.agent.update_servers(known_servers + ["10.1.10.200:4829"]) - assert "10.1.10.200:4829" in nomad_setup.agent.get_servers() - except nomad_exceptions.BaseNomadException: - pass - - def test_force_leave(nomad_setup): r = nomad_setup.agent.force_leave("nope") assert r == 200 diff --git a/tests/test_allocation.py b/tests/test_allocation.py index c8c9a36..1aca38c 100644 --- a/tests/test_allocation.py +++ b/tests/test_allocation.py @@ -3,6 +3,7 @@ import uuid import responses import tests.common as common +import os # integration tests requires nomad Vagrant VM or Binary running @@ -19,6 +20,15 @@ def test_get_allocation(nomad_setup): assert isinstance(nomad_setup.allocation.get_allocation(id), dict) == True +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 9, 2), + reason="Nomad alloc stop not supported", +) +def test_stop_allocation(nomad_setup): + id = nomad_setup.job.get_allocations("example")[0]["ID"] + assert isinstance(nomad_setup.allocation.stop_allocation(id), dict) == True + + def test_dunder_getitem_exist(nomad_setup): id = nomad_setup.job.get_allocations("example")[0]["ID"] a = nomad_setup.allocation[id] @@ -62,8 +72,21 @@ def test_dunder_getattr(nomad_setup): def test_get_allocation_with_namespace(nomad_setup_with_namespace): responses.add( responses.GET, - "http://{ip}:{port}/v1/allocation/a8198d79-cfdb-6593-a999-1e9adabcba2e?namespace={namespace}".format(ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE), + "http://{ip}:{port}/v1/allocation/a8198d79-cfdb-6593-a999-1e9adabcba2e?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), status=200, - json={"ID": "a8198d79-cfdb-6593-a999-1e9adabcba2e","EvalID": "5456bd7a-9fc0-c0dd-6131-cbee77f57577","Namespace": common.NOMAD_NAMESPACE, "Name": "example.cache[0]","NodeID": "fb2170a8-257d-3c64-b14d-bc06cc94e34c","PreviousAllocation": "516d2753-0513-cfc7-57ac-2d6fac18b9dc","NextAllocation": "cd13d9b9-4f97-7184-c88b-7b451981616b"} + json={ + "ID": "a8198d79-cfdb-6593-a999-1e9adabcba2e", + "EvalID": "5456bd7a-9fc0-c0dd-6131-cbee77f57577", + "Namespace": common.NOMAD_NAMESPACE, + "Name": "example.cache[0]", + "NodeID": "fb2170a8-257d-3c64-b14d-bc06cc94e34c", + "PreviousAllocation": "516d2753-0513-cfc7-57ac-2d6fac18b9dc", + "NextAllocation": "cd13d9b9-4f97-7184-c88b-7b451981616b", + }, + ) + assert ( + common.NOMAD_NAMESPACE + in nomad_setup_with_namespace.allocation.get_allocation("a8198d79-cfdb-6593-a999-1e9adabcba2e")["Namespace"] ) - assert common.NOMAD_NAMESPACE in nomad_setup_with_namespace.allocation.get_allocation("a8198d79-cfdb-6593-a999-1e9adabcba2e")["Namespace"] diff --git a/tests/test_allocations.py b/tests/test_allocations.py index 962a1d7..186a41d 100644 --- a/tests/test_allocations.py +++ b/tests/test_allocations.py @@ -22,6 +22,9 @@ def test_get_allocations_prefix(nomad_setup): prefix = allocations[0]["ID"][:4] nomad_setup.allocations.get_allocations(prefix=prefix) +def test_get_allocations_resouces(nomad_setup): + allocations = nomad_setup.allocations.get_allocations(resources=True) + assert "AllocatedResources" in allocations[0] def test_dunder_str(nomad_setup): assert isinstance(str(nomad_setup.allocations), str) @@ -38,7 +41,7 @@ def test_dunder_getattr(nomad_setup): def test_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.allocations, '__iter__') + assert hasattr(nomad_setup.allocations, "__iter__") for j in nomad_setup.allocations: pass @@ -54,8 +57,47 @@ def test_dunder_len(nomad_setup): def test_get_allocations_with_namespace(nomad_setup_with_namespace): responses.add( responses.GET, - "http://{ip}:{port}/v1/allocations?namespace={namespace}".format(ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE), + "http://{ip}:{port}/v1/allocations?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), status=200, - json=[{"ID": "a8198d79-cfdb-6593-a999-1e9adabcba2e","EvalID": "5456bd7a-9fc0-c0dd-6131-cbee77f57577","Namespace": common.NOMAD_NAMESPACE, "Name": "example.cache[0]","NodeID": "fb2170a8-257d-3c64-b14d-bc06cc94e34c","PreviousAllocation": "516d2753-0513-cfc7-57ac-2d6fac18b9dc","NextAllocation": "cd13d9b9-4f97-7184-c88b-7b451981616b"}] + json=[ + { + "ID": "a8198d79-cfdb-6593-a999-1e9adabcba2e", + "EvalID": "5456bd7a-9fc0-c0dd-6131-cbee77f57577", + "Namespace": common.NOMAD_NAMESPACE, + "Name": "example.cache[0]", + "NodeID": "fb2170a8-257d-3c64-b14d-bc06cc94e34c", + "PreviousAllocation": "516d2753-0513-cfc7-57ac-2d6fac18b9dc", + "NextAllocation": "cd13d9b9-4f97-7184-c88b-7b451981616b", + } + ], ) assert common.NOMAD_NAMESPACE in nomad_setup_with_namespace.allocations.get_allocations()[0]["Namespace"] + + +@responses.activate +def test_get_allocations_with_namespace_override_namespace_declared_on_create(nomad_setup_with_namespace): + override_namespace_name = "namespace=override-namespace" + responses.add( + responses.GET, + "http://{ip}:{port}/v1/allocations?prefix=a8198d79-cfdb-6593-a999-1e9adabcba2e&namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=override_namespace_name + ), + status=200, + json=[ + { + "ID": "a8198d79-cfdb-6593-a999-1e9adabcba2e", + "EvalID": "5456bd7a-9fc0-c0dd-6131-cbee77f57577", + "Namespace": override_namespace_name, + "Name": "example.cache[0]", + "NodeID": "fb2170a8-257d-3c64-b14d-bc06cc94e34c", + "PreviousAllocation": "516d2753-0513-cfc7-57ac-2d6fac18b9dc", + "NextAllocation": "cd13d9b9-4f97-7184-c88b-7b451981616b", + } + ], + ) + + nomad_setup_with_namespace.allocations.get_allocations( + "a8198d79-cfdb-6593-a999-1e9adabcba2e", namespace=override_namespace_name + ) diff --git a/tests/test_base.py b/tests/test_base.py index 60b1071..df8f44a 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,9 +1,13 @@ -import pytest -import tests.common as common -import nomad import os + +import mock +import pytest +import requests import responses +import nomad +import tests.common as common + def test_base_region_qs(): n = nomad.Nomad(host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN, region="random") @@ -14,7 +18,14 @@ def test_base_region_qs(): def test_base_region_and_namespace_qs(): - n = nomad.Nomad(host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN, region="random", namespace="test") + n = nomad.Nomad( + host=common.IP, + port=common.NOMAD_PORT, + verify=False, + token=common.NOMAD_TOKEN, + region="random", + namespace="test", + ) qs = n.jobs._query_string_builder("v1/jobs") assert "region" in qs @@ -24,45 +35,136 @@ def test_base_region_and_namespace_qs(): assert qs["namespace"] == "test" +def test_base_region_and_namespace_qs_namespace_override(): + n = nomad.Nomad( + host=common.IP, + port=common.NOMAD_PORT, + verify=False, + token=common.NOMAD_TOKEN, + region="random", + namespace="test", + ) + qs = n.jobs._query_string_builder("v1/jobs", {"namespace": "new-namespace"}) + + assert "namespace" not in qs + assert "region" in qs + assert qs["region"] == "random" + + +def test_base_region_and_namespace_qs_region_override(): + n = nomad.Nomad( + host=common.IP, + port=common.NOMAD_PORT, + verify=False, + token=common.NOMAD_TOKEN, + region="random", + namespace="test", + ) + qs = n.jobs._query_string_builder("v1/jobs", {"region": "new-region"}) + + assert "region" not in qs + assert "namespace" in qs + assert qs["namespace"] == "test" + + +def test_base_region_and_namespace_qs_overrides_via_params(): + n = nomad.Nomad( + host=common.IP, + port=common.NOMAD_PORT, + verify=False, + token=common.NOMAD_TOKEN, + region="random", + namespace="test", + ) + qs = n.jobs._query_string_builder("v1/jobs", {"namespace": "new-namespace", "region": "new-region"}) + + assert qs == {} + + # integration tests requires nomad Vagrant VM or Binary running def test_base_get_connection_error(): - n = nomad.Nomad( - host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) + n = nomad.Nomad(host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) with pytest.raises(nomad.api.exceptions.BaseNomadException): j = n.evaluations["nope"] def test_base_put_connection_error(): - n = nomad.Nomad( - host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) + n = nomad.Nomad(host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) with pytest.raises(nomad.api.exceptions.BaseNomadException): j = n.system.initiate_garbage_collection() def test_base_delete_connection_error(): - n = nomad.Nomad( - host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) + n = nomad.Nomad(host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) with pytest.raises(nomad.api.exceptions.BaseNomadException): j = n.job.deregister_job("example") -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported") +@mock.patch("nomad.api.base.requests.Session") +def test_base_raise_exception_not_requests_response_object(mock_requests): + mock_requests().delete.side_effect = [requests.RequestException()] + + with pytest.raises(nomad.api.exceptions.BaseNomadException) as excinfo: + n = nomad.Nomad(host="162.16.10.102", port=common.NOMAD_PORT, timeout=0.001, verify=False) + + _ = n.job.deregister_job("example") + + # excinfo is a ExceptionInfo instance, which is a wrapper around the actual exception raised. + # The main attributes of interest are .type, .value and .traceback. + # https://docs.pytest.org/en/3.0.1/assert.html#assertions-about-expected-exceptions + assert hasattr(excinfo.value.nomad_resp, "text") is False + assert isinstance(excinfo.value.nomad_resp, requests.RequestException) + assert "raised due" in str(excinfo.value) + + +def test_base_raise_exception_is_requests_response_object(nomad_setup): + with pytest.raises(nomad.api.exceptions.BaseNomadException) as excinfo: + _ = nomad_setup.job.get_job("examplezz") + + # excinfo is a ExceptionInfo instance, which is a wrapper around the actual exception raised. + # The main attributes of interest are .type, .value and .traceback. + # https://docs.pytest.org/en/3.0.1/assert.html#assertions-about-expected-exceptions + assert hasattr(excinfo.value.nomad_resp, "text") is True + assert isinstance(excinfo.value.nomad_resp, requests.Response) + assert "raised with" in str(excinfo.value) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 0), reason="Nomad dispatch not supported" +) def test_base_get_connnection_not_authorized(): - n = nomad.Nomad( - host=common.IP, port=common.NOMAD_PORT, token='aed2fc63-c155-40d5-b58a-18deed4b73e5', verify=False) + n = nomad.Nomad(host=common.IP, port=common.NOMAD_PORT, token="aed2fc63-c155-40d5-b58a-18deed4b73e5", verify=False) with pytest.raises(nomad.api.exceptions.URLNotAuthorizedNomadException): j = n.job.get_job("example") @responses.activate def test_base_use_address_instead_on_host_port(): - responses.add( - responses.GET, - 'https://nomad.service.consul:4646/v1/jobs', - status=200, - json=[] + responses.add(responses.GET, "https://nomad.service.consul:4646/v1/jobs", status=200, json=[]) + + nomad_address = "https://nomad.service.consul:4646" + n = nomad.Nomad( + address=nomad_address, host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN ) + n.jobs.get_jobs() + +@responses.activate +def test_use_custom_user_agent(): + custom_agent_name = "custom_agent" + responses.add(responses.GET, "https://nomad.service.consul:4646/v1/jobs", status=200, json=[]) nomad_address = "https://nomad.service.consul:4646" - n = nomad.Nomad(address=nomad_address, host=common.IP, port=common.NOMAD_PORT, verify=False, token=common.NOMAD_TOKEN) + n = nomad.Nomad( + address=nomad_address, host=common.IP, port=common.NOMAD_PORT, verify=False, + token=common.NOMAD_TOKEN, user_agent=custom_agent_name + ) n.jobs.get_jobs() + + assert responses.calls[0].request.headers["User-Agent"] == custom_agent_name + +def test_env_variables(): + # This ensures that the env variables are only read upon initialization of Nomad() instance, + # and not before. + with mock.patch.dict(os.environ, {"NOMAD_ADDR": "https://foo"}): + n = nomad.Nomad() + assert n.address == os.environ["NOMAD_ADDR"] diff --git a/tests/test_client.py b/tests/test_client.py index d585bb0..5330869 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,6 @@ import pytest import json +import signal import time import os @@ -8,6 +9,21 @@ from flaky import flaky +def get_running_allocation(nomad_setup): + max_iterations = 6 + for _ in range(max_iterations): + try: + return next( + alloc + for alloc in nomad_setup.allocations.get_allocations() + if alloc["ClientStatus"] == "running" + ) + except StopIteration: + # No alloc running + time.sleep(5) + raise ValueError("No allocations running") + + # integration tests requires nomad Vagrant VM or Binary running def test_register_job(nomad_setup): @@ -16,78 +32,90 @@ def test_register_job(nomad_setup): nomad_setup.job.register_job("example", job) assert "example" in nomad_setup.job - time.sleep(20) + max_iterations = 6 + while nomad_setup.job["example"]["Status"] != "running": + time.sleep(5) + if max_iterations == 0: + raise Exception("register_job, job 'example' did not start") + + max_iterations -= 1 -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version" +) def test_ls_list_files(nomad_setup): a = nomad_setup.allocations.get_allocations()[0]["ID"] f = nomad_setup.client.ls.list_files(a) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version" +) def test_stat_stat_file(nomad_setup): a = nomad_setup.allocations.get_allocations()[0]["ID"] f = nomad_setup.client.stat.stat_file(a) -@flaky(max_runs=5, min_passes=1) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version") -def test_cat_read_file(nomad_setup): - - a = nomad_setup.allocations.get_allocations()[0]["ID"] - f = nomad_setup.client.cat.read_file(a, "/redis/executor.out") - - -@flaky(max_runs=5, min_passes=1) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 1), reason="Not supported in version") -def test_read_file_offset(nomad_setup): - - a = nomad_setup.allocations.get_allocations()[0]["ID"] - _ = nomad_setup.client.read_at.read_file_offset(a, 1, 10, "/redis/executor.out") - - -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version" +) def test_streamfile_fail(nomad_setup): with pytest.raises(nomad.api.exceptions.BadRequestNomadException): a = nomad_setup.allocations.get_allocations()[0]["ID"] - _ = nomad_setup.client.stream_file.stream(a, 1, "start", "/redis/executor") #invalid file name - - -@flaky(max_runs=5, min_passes=1) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") -def test_streamlogs(nomad_setup): - - a = nomad_setup.allocations.get_allocations()[0]["ID"] - _ = nomad_setup.client.stream_logs.stream(a, "redis", "stderr", False) + _ = nomad_setup.client.stream_file.stream(a, 1, "start", "/redis/executor") # invalid file name -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version" +) def test_read_stats(nomad_setup): f = nomad_setup.client.stats.read_stats() -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 6), reason="Not supported in version" +) def test_read_allocation_stats(nomad_setup): a = nomad_setup.allocations.get_allocations()[0]["ID"] f = nomad_setup.client.allocation.read_allocation_stats(a) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") -def test_gc_allocation_fail(nomad_setup): +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 9, 1), reason="Not supported in version" +) +def test_signal_allocation(nomad_setup): + alloc_id = get_running_allocation(nomad_setup)["ID"] + nomad_setup.client.allocation.signal_allocation(alloc_id, signal.SIGUSR1.name) - a = nomad_setup.allocations.get_allocations()[0]["ID"] - with pytest.raises(nomad.api.exceptions.URLNotFoundNomadException): - f = nomad_setup.client.gc_allocation.garbage_collect(a) # attempt on non-stopped allocation +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 9, 1), reason="Not supported in version" +) +def test_signal_allocation_task(nomad_setup): + allocation = get_running_allocation(nomad_setup) + alloc_id = allocation["ID"] + task = list(allocation["TaskStates"].keys())[0] + nomad_setup.client.allocation.signal_allocation(alloc_id, signal.SIGUSR1.name, task) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") -def test_gc_all_allocations(nomad_setup): +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 9, 1), reason="Not supported in version" +) +def test_signal_allocation_invalid_signal(nomad_setup): + alloc_id = get_running_allocation(nomad_setup)["ID"] + with pytest.raises(nomad.api.exceptions.BaseNomadException, match="invalid signal"): + nomad_setup.client.allocation.signal_allocation(alloc_id, "INVALID-SIGNAL") + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version" +) +def test_gc_all_allocations(nomad_setup): node_id = nomad_setup.nodes.get_nodes()[0]["ID"] nomad_setup.client.gc_all_allocations.garbage_collect(node_id) nomad_setup.client.gc_all_allocations.garbage_collect() diff --git a/tests/test_deployment.py b/tests/test_deployment.py index e9a7640..8e0d1ca 100644 --- a/tests/test_deployment.py +++ b/tests/test_deployment.py @@ -16,14 +16,18 @@ def test_register_job(nomad_setup): # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_get_deployment(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] assert isinstance(nomad_setup.deployment.get_deployment(deploymentID), dict) assert deploymentID == nomad_setup.deployment.get_deployment(deploymentID)["ID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_get_deployment_allocations(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] assert isinstance(nomad_setup.deployment.get_deployment_allocations(deploymentID), list) @@ -31,7 +35,9 @@ def test_get_deployment_allocations(nomad_setup): assert "example" == nomad_setup.deployment.get_deployment_allocations(deploymentID)[0]["JobID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_fail_deployment(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] try: @@ -40,7 +46,9 @@ def test_fail_deployment(nomad_setup): assert err.nomad_resp.text == "can't fail terminal deployment" -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_pause_deployment(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] try: @@ -49,7 +57,9 @@ def test_pause_deployment(nomad_setup): assert err.nomad_resp.text == "can't resume terminal deployment" -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_promote_all_deployment(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] try: @@ -58,7 +68,9 @@ def test_promote_all_deployment(nomad_setup): assert err.nomad_resp.text == "can't promote terminal deployment" -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_promote_all_deployment(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] try: @@ -67,7 +79,9 @@ def test_promote_all_deployment(nomad_setup): assert err.nomad_resp.text == "can't promote terminal deployment" -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_deployment_allocation_health(nomad_setup): deploymentID = nomad_setup.deployments.get_deployments()[0]["ID"] allocationID = nomad_setup.deployment.get_deployment(deploymentID)["ID"] @@ -111,6 +125,7 @@ def test_dunder_getattr(nomad_setup): with pytest.raises(AttributeError): _ = nomad_setup.deployment.does_not_exist + @responses.activate # # fix No data when you are using namespaces #82 @@ -118,8 +133,48 @@ def test_dunder_getattr(nomad_setup): def test_get_deployment_with_namespace(nomad_setup_with_namespace): responses.add( responses.GET, - "http://{ip}:{port}/v1/deployment/a8198d79-cfdb-6593-a999-1e9adabcba2e?namespace={namespace}".format(ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE), + "http://{ip}:{port}/v1/deployment/a8198d79-cfdb-6593-a999-1e9adabcba2e?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), status=200, - json={"ID": "70638f62-5c19-193e-30d6-f9d6e689ab8e","JobID": "example", "JobVersion": 1, "JobModifyIndex": 17, "JobSpecModifyIndex": 17, "JobCreateIndex": 7,"Namespace": common.NOMAD_NAMESPACE, "Name": "example.cache[0]"} + json={ + "ID": "70638f62-5c19-193e-30d6-f9d6e689ab8e", + "JobID": "example", + "JobVersion": 1, + "JobModifyIndex": 17, + "JobSpecModifyIndex": 17, + "JobCreateIndex": 7, + "Namespace": common.NOMAD_NAMESPACE, + "Name": "example.cache[0]", + }, + ) + assert ( + common.NOMAD_NAMESPACE + in nomad_setup_with_namespace.deployment.get_deployment("a8198d79-cfdb-6593-a999-1e9adabcba2e")["Namespace"] + ) + + +@responses.activate +def test_get_deployments_with_namespace_override_namespace_declared_on_create(nomad_setup_with_namespace): + override_namespace_name = "override-namespace" + responses.add( + responses.GET, + "http://{ip}:{port}/v1/deployments?prefix=a8198d79-cfdb-6593-a999-1e9adabcba2e&namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=override_namespace_name + ), + status=200, + json={ + "ID": "70638f62-5c19-193e-30d6-f9d6e689ab8e", + "JobID": "example", + "JobVersion": 1, + "JobModifyIndex": 17, + "JobSpecModifyIndex": 17, + "JobCreateIndex": 7, + "Namespace": override_namespace_name, + "Name": "example.cache[0]", + }, + ) + + nomad_setup_with_namespace.deployments.get_deployments( + "a8198d79-cfdb-6593-a999-1e9adabcba2e", namespace=override_namespace_name ) - assert common.NOMAD_NAMESPACE in nomad_setup_with_namespace.deployment.get_deployment("a8198d79-cfdb-6593-a999-1e9adabcba2e")["Namespace"] diff --git a/tests/test_deployments.py b/tests/test_deployments.py index b361d71..258aedd 100644 --- a/tests/test_deployments.py +++ b/tests/test_deployments.py @@ -14,48 +14,64 @@ def test_register_job(nomad_setup): # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_get_evaluation(nomad_setup): assert "example" == nomad_setup.deployments.get_deployments()[0]["JobID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_get_deployments_prefix(nomad_setup): deployments = nomad_setup.deployments.get_deployments() prefix = deployments[0]["ID"][:4] nomad_setup.deployments.get_deployments(prefix=prefix) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_getitem_exist(nomad_setup): jobID = nomad_setup.deployments.get_deployments()[0]["ID"] d = nomad_setup.deployment[jobID] assert isinstance(d, dict) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_getitem_not_exist(nomad_setup): with pytest.raises(KeyError): _ = nomad_setup.deployments["nope"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_contain_exists(nomad_setup): jobID = nomad_setup.deployments.get_deployments()[0]["ID"] assert jobID in nomad_setup.deployments -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_contain_not_exist(nomad_setup): assert "nope" not in nomad_setup.deployments -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_len(nomad_setup): assert len(nomad_setup.deployments) == 1 -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_dunder_iter(nomad_setup): for d in nomad_setup.deployments: pass @@ -74,6 +90,7 @@ def test_dunder_getattr(nomad_setup): with pytest.raises(AttributeError): _ = nomad_setup.deployments.does_not_exist + @responses.activate # # fix No data when you are using namespaces #82 @@ -81,8 +98,21 @@ def test_dunder_getattr(nomad_setup): def test_get_deployments_with_namespace(nomad_setup_with_namespace): responses.add( responses.GET, - "http://{ip}:{port}/v1/deployments?namespace={namespace}".format(ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE), + "http://{ip}:{port}/v1/deployments?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), status=200, - json=[{"ID": "70638f62-5c19-193e-30d6-f9d6e689ab8e","JobID": "example", "JobVersion": 1, "JobModifyIndex": 17, "JobSpecModifyIndex": 17, "JobCreateIndex": 7,"Namespace": common.NOMAD_NAMESPACE, "Name": "example.cache[0]"}] + json=[ + { + "ID": "70638f62-5c19-193e-30d6-f9d6e689ab8e", + "JobID": "example", + "JobVersion": 1, + "JobModifyIndex": 17, + "JobSpecModifyIndex": 17, + "JobCreateIndex": 7, + "Namespace": common.NOMAD_NAMESPACE, + "Name": "example.cache[0]", + } + ], ) assert common.NOMAD_NAMESPACE in nomad_setup_with_namespace.deployments.get_deployments()[0]["Namespace"] diff --git a/tests/test_evaluation.py b/tests/test_evaluation.py index 8a80e43..ec16dac 100644 --- a/tests/test_evaluation.py +++ b/tests/test_evaluation.py @@ -14,8 +14,7 @@ def test_register_job(nomad_setup): # integration tests requires nomad Vagrant VM or Binary running def test_get_evaluation(nomad_setup): evalID = nomad_setup.job.get_allocations("example")[0]["EvalID"] - assert isinstance( - nomad_setup.evaluation.get_evaluation(evalID), dict) == True + assert isinstance(nomad_setup.evaluation.get_evaluation(evalID), dict) == True def test_get_allocations(nomad_setup): diff --git a/tests/test_evaluations.py b/tests/test_evaluations.py index 4ca4362..e3befa8 100644 --- a/tests/test_evaluations.py +++ b/tests/test_evaluations.py @@ -57,7 +57,7 @@ def test_dunder_getattr(nomad_setup): def test_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.evaluations, '__iter__') + assert hasattr(nomad_setup.evaluations, "__iter__") for j in nomad_setup.evaluations: pass diff --git a/tests/test_event.py b/tests/test_event.py new file mode 100644 index 0000000..3a2fd3b --- /dev/null +++ b/tests/test_event.py @@ -0,0 +1,50 @@ +import json + +from flaky import flaky + + +# integration tests requires nomad Vagrant VM or Binary running +def test_register_job(nomad_setup): + + with open("example.json") as fh: + job = json.loads(fh.read()) + nomad_setup.job.register_job("example", job) + assert "example" in nomad_setup.job + + +@flaky(max_runs=5, min_passes=1) +def test_get_event_stream_default(nomad_setup): + + stream, stream_exit, events = nomad_setup.event.stream.get_stream() + stream.daemon = True + stream.start() + + test_register_job(nomad_setup) + event = events.get(timeout=5) + assert event + assert "Index" in event + + stream_exit.set() + + +@flaky(max_runs=5, min_passes=1) +def test_get_event_stream_with_customized_topic(nomad_setup): + stream, stream_exit, events = nomad_setup.event.stream.get_stream(topic={"Node": "*"}) + stream.daemon = True + stream.start() + + node_id = nomad_setup.nodes.get_nodes()[0]["ID"] + nomad_setup.node.drain_node_with_spec(node_id, None) + + event = events.get(timeout=5) + assert event + assert "Index" in event + assert event["Events"][0]["Type"] in ( + "NodeRegistration", + "NodeDeregistration", + "NodeEligibility", + "NodeDrain", + "NodeEvent", + ) + + stream_exit.set() diff --git a/tests/test_job.py b/tests/test_job.py index 792f734..ae1eb36 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -1,7 +1,15 @@ -import pytest -import nomad import json import os +import uuid + +import pytest +import responses + +import nomad +import tests.common as common + +from flaky import flaky + from nomad.api import exceptions @@ -18,6 +26,79 @@ def test_get_job(nomad_setup): assert isinstance(nomad_setup.job.get_job("example"), dict) == True +@responses.activate +def test_get_jobs_with_namespace_override_no_namespace_declared_on_create_incorrect_declared_namespace(nomad_setup): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/job/18a0f501-41d5-ae43-ff61-1d8ec3ec8314?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + with pytest.raises(exceptions.BaseNomadException): + nomad_setup.job.get_job(id_=str(uuid.uuid4())) + + +@responses.activate +def test_get_jobs_with_namespace_override_no_namespace_declared_on_create(nomad_setup): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/job/18a0f501-41d5-ae43-ff61-1d8ec3ec8314?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + nomad_setup.job.get_job(id_="18a0f501-41d5-ae43-ff61-1d8ec3ec8314", namespace=common.NOMAD_NAMESPACE) + + +@responses.activate +def test_get_jobs_with_namespace_override_namespace_declared_on_create(nomad_setup_with_namespace): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/job/18a0f501-41d5-ae43-ff61-1d8ec3ec8314?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace="override-namespace" + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + nomad_setup_with_namespace.job.get_job(id_="18a0f501-41d5-ae43-ff61-1d8ec3ec8314", namespace="override-namespace") + + def test_get_allocations(nomad_setup): j = nomad_setup.job["example"] a = nomad_setup.job.get_allocations("example") @@ -33,6 +114,7 @@ def test_get_evaluations(nomad_setup): def test_evaluate_job(nomad_setup): assert "EvalID" in nomad_setup.job.evaluate_job("example") + # def test_periodic_job(nomad_setup): # assert "EvalID" in nomad_setup.job.periodic_job("example") @@ -42,73 +124,83 @@ def test_delete_job(nomad_setup): test_register_job(nomad_setup) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 3), reason="Nomad dispatch not supported") +@flaky(max_runs=5, min_passes=1) def test_dispatch_job(nomad_setup): with open("example_batch_parameterized.json") as fh: job = json.loads(fh.read()) nomad_setup.job.register_job("example-batch", job) try: - nomad_setup.job.dispatch_job("example-batch", meta={"time": "500"}) - except (exceptions.URLNotFoundNomadException, - exceptions.BaseNomadException) as e: + nomad_setup.job.dispatch_job("example-batch", meta={"time": "500"}, id_prefix_template="run1") + except (exceptions.URLNotFoundNomadException, exceptions.BaseNomadException) as e: print(e.nomad_resp.text) raise e assert "example-batch" in nomad_setup.job -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 3), reason="Nomad dispatch not supported") +@flaky(max_runs=5, min_passes=1) +def test_dispatch_job_idempotency(nomad_setup): + with open("example_batch_parameterized.json") as fh: + job = json.loads(fh.read()) + nomad_setup.job.register_job("example-batch-idempotent", job) + + # First dispatch should succeed + try: + nomad_setup.job.dispatch_job("example-batch-idempotent", meta={"time": "500"}, id_prefix_template="run1", idempotency_token="737ae8cd-f237-43a5-8fad-0e6a3f94ad55") + except (exceptions.URLNotFoundNomadException, exceptions.BaseNomadException) as e: + print(e.nomad_resp.text) + raise e + assert "example-batch-idempotent" in nomad_setup.job + + # Second dispatch with the same idempotency token should fail + with pytest.raises(exceptions.BaseNomadException): + nomad_setup.job.dispatch_job("example-batch-idempotent", meta={"time": "500"}, id_prefix_template="run2", idempotency_token="737ae8cd-f237-43a5-8fad-0e6a3f94ad55") + + def test_summary_job(nomad_setup): j = nomad_setup.job["example"] assert "JobID" in nomad_setup.job.get_summary(j["ID"]) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 4, 0), reason="Not supported in version") def test_plan_job(nomad_setup): with open("example.json") as fh: job = json.loads(fh.read()) - assert "Index" in nomad_setup.job.plan_job(nomad_setup.job["example"]["ID"],job) + assert "Index" in nomad_setup.job.plan_job(nomad_setup.job["example"]["ID"], job) + -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") def test_versions_job(nomad_setup): assert "Versions" in nomad_setup.job.get_versions("example") -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") + def test_versions_job_missing(nomad_setup): with pytest.raises(nomad.api.exceptions.URLNotFoundNomadException): assert "Versions" in nomad_setup.job.get_versions("example1") -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") + def test_get_job_deployments(nomad_setup): assert "JobID" in nomad_setup.job.get_deployments("example")[0] assert isinstance(nomad_setup.job.get_deployments("example"), list) assert isinstance(nomad_setup.job.get_deployments("example")[0], dict) assert "example" == nomad_setup.job.get_deployments("example")[0]["JobID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") + def test_get_job_deployment(nomad_setup): assert "JobID" in nomad_setup.job.get_deployment("example") assert isinstance(nomad_setup.job.get_deployment("example"), dict) assert "example" == nomad_setup.job.get_deployment("example")["JobID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") -def test_get_job_deployment(nomad_setup): + +def test_get_summary(nomad_setup): assert "JobID" in nomad_setup.job.get_summary("example") assert isinstance(nomad_setup.job.get_summary("example"), dict) assert "example" == nomad_setup.job.get_summary("example")["JobID"] -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") -def test_revert_job(nomad_setup): - current_job_version = nomad_setup.job.get_deployment("example")["JobVersion"] - prior_job_version = current_job_version - 1 - nomad_setup.job.revert_job("example", prior_job_version, current_job_version) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") def test_revert_job(nomad_setup): current_job_version = nomad_setup.job.get_deployment("example")["JobVersion"] prior_job_version = current_job_version - 1 nomad_setup.job.revert_job("example", prior_job_version, current_job_version) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") + def test_stable_job(nomad_setup): current_job_version = nomad_setup.job.get_deployment("example")["JobVersion"] nomad_setup.job.stable_job("example", current_job_version, True) @@ -146,9 +238,11 @@ def test_dunder_getattr(nomad_setup): with pytest.raises(AttributeError): d = nomad_setup.job.does_not_exist + def test_delete_job_with_invalid_purge_param_raises(nomad_setup): with pytest.raises(exceptions.InvalidParameters): - nomad_setup.job.deregister_job("example", purge='True') + nomad_setup.job.deregister_job("example", purge="True") + def test_delete_job_with_purge(nomad_setup): # Run this last since it will purge the job completely, resetting things like diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 7adabc3..05d5895 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -4,6 +4,10 @@ import responses import tests.common as common + +from nomad.api.exceptions import BaseNomadException + + # integration tests requires nomad Vagrant VM or Binary running def test_register_job(nomad_setup): @@ -21,7 +25,9 @@ def test_get_jobs_prefix(nomad_setup): nomad_setup.jobs.get_jobs(prefix="ex") -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 3), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 3), reason="Not supported in version" +) def test_parse_job(nomad_setup): with open("example.nomad") as fh: hcl = fh.read() @@ -64,7 +70,7 @@ def test_dunder_getattr(nomad_setup): def test_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.jobs, '__iter__') + assert hasattr(nomad_setup.jobs, "__iter__") for j in nomad_setup.jobs: pass @@ -72,15 +78,99 @@ def test_dunder_iter(nomad_setup): def test_dunder_len(nomad_setup): assert len(nomad_setup.jobs) >= 0 -@responses.activate -# + # fix No data when you are using namespaces #82 -# +@responses.activate def test_get_jobs_with_namespace(nomad_setup_with_namespace): responses.add( responses.GET, - "http://{ip}:{port}/v1/jobs?namespace={namespace}".format(ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE), + "http://{ip}:{port}/v1/jobs?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), status=200, - json=[{"Region": "global","ID": "my-job", "ParentID": "", "Name": "my-job","Namespace": common.NOMAD_NAMESPACE, "Type": "batch", "Priority": 50}] + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], ) assert common.NOMAD_NAMESPACE in nomad_setup_with_namespace.jobs.get_jobs()[0]["Namespace"] + + +@responses.activate +def test_get_jobs_with_namespace_override_no_namespace_declared_on_create_incorrect_declared_namespace(nomad_setup): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/jobs?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + with pytest.raises(BaseNomadException): + nomad_setup.jobs.get_jobs(namespace="should-raise") + + +@responses.activate +def test_get_jobs_with_namespace_override_no_namespace_declared_on_create(nomad_setup): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/jobs?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace=common.NOMAD_NAMESPACE + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + nomad_setup.jobs.get_jobs(namespace=common.NOMAD_NAMESPACE) + + +@responses.activate +def test_get_jobs_with_namespace_override_namespace_declared_on_create(nomad_setup_with_namespace): + responses.add( + responses.GET, + "http://{ip}:{port}/v1/jobs?namespace={namespace}".format( + ip=common.IP, port=common.NOMAD_PORT, namespace="override-namespace" + ), + status=200, + json=[ + { + "Region": "global", + "ID": "my-job", + "ParentID": "", + "Name": "my-job", + "Namespace": common.NOMAD_NAMESPACE, + "Type": "batch", + "Priority": 50, + } + ], + ) + + nomad_setup_with_namespace.jobs.get_jobs(namespace="override-namespace") diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 9d7bc00..618db14 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -3,7 +3,9 @@ # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 1), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 7, 1), reason="Not supported in version" +) def test_metrics(nomad_setup): nomad_setup.metrics.get_metrics() @@ -19,4 +21,4 @@ def test_dunder_repr(nomad_setup): def test_dunder_getattr(nomad_setup): with pytest.raises(AttributeError): - d = nomad_setup.metrics.does_not_exist \ No newline at end of file + d = nomad_setup.metrics.does_not_exist diff --git a/tests/test_namespace.py b/tests/test_namespace.py index 0d30571..53a3924 100644 --- a/tests/test_namespace.py +++ b/tests/test_namespace.py @@ -8,24 +8,20 @@ @responses.activate def test_create_namespace(nomad_setup): - responses.add( - responses.POST, - "http://{ip}:{port}/v1/namespace".format(ip=common.IP, port=common.NOMAD_PORT), - status=200 - ) + responses.add( + responses.POST, "http://{ip}:{port}/v1/namespace".format(ip=common.IP, port=common.NOMAD_PORT), status=200 + ) - namespace_api = '{"Name":"api","Description":"api server namespace"}' - namespace = json.loads(namespace_api) - nomad_setup.namespace.create_namespace(namespace) + namespace_api = '{"Name":"api","Description":"api server namespace"}' + namespace = json.loads(namespace_api) + nomad_setup.namespace.create_namespace(namespace) @responses.activate def test_update_namespace(nomad_setup): responses.add( - responses.POST, - "http://{ip}:{port}/v1/namespace/api".format(ip=common.IP, port=common.NOMAD_PORT), - status=200 + responses.POST, "http://{ip}:{port}/v1/namespace/api".format(ip=common.IP, port=common.NOMAD_PORT), status=200 ) namespace_api = '{"Name":"api","Description":"updated namespace"}' @@ -40,7 +36,7 @@ def test_get_namespace(nomad_setup): responses.GET, "http://{ip}:{port}/v1/namespace/api".format(ip=common.IP, port=common.NOMAD_PORT), status=200, - json={"Name": "api", "Description": "api server namespace"} + json={"Name": "api", "Description": "api server namespace"}, ) assert "api" in nomad_setup.namespace.get_namespace("api")["Name"] @@ -57,7 +53,6 @@ def test_delete_namespace(nomad_setup): nomad_setup.namespace.delete_namespace("api") - ######### ENTERPRISE TEST ########### # def test_apply_namespace(nomad_setup): # namespace_api='{"Name":"api","Description":"api server namespace"}' diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index 393baa8..46e51f2 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -12,19 +12,9 @@ def test_get_namespaces(nomad_setup): "http://{ip}:{port}/v1/namespaces".format(ip=common.IP, port=common.NOMAD_PORT), status=200, json=[ - { - "CreateIndex": 31, - "Description": "Production API Servers", - "ModifyIndex": 31, - "Name": "api-prod" - }, - { - "CreateIndex": 5, - "Description": "Default shared namespace", - "ModifyIndex": 5, - "Name": "default" - } - ] + {"CreateIndex": 31, "Description": "Production API Servers", "ModifyIndex": 31, "Name": "api-prod"}, + {"CreateIndex": 5, "Description": "Default shared namespace", "ModifyIndex": 5, "Name": "default"}, + ], ) assert isinstance(nomad_setup.namespaces.get_namespaces(), list) == True @@ -37,13 +27,8 @@ def test_get_namespaces_prefix(nomad_setup): "http://{ip}:{port}/v1/namespaces?prefix=api-".format(ip=common.IP, port=common.NOMAD_PORT), status=200, json=[ - { - "CreateIndex": 31, - "Description": "Production API Servers", - "ModifyIndex": 31, - "Name": "api-prod" - }, - ] + {"CreateIndex": 31, "Description": "Production API Servers", "ModifyIndex": 31, "Name": "api-prod"}, + ], ) assert isinstance(nomad_setup.namespaces.get_namespaces(prefix="api-"), list) == True @@ -56,19 +41,9 @@ def test_namespaces_iter(nomad_setup): "http://{ip}:{port}/v1/namespaces".format(ip=common.IP, port=common.NOMAD_PORT), status=200, json=[ - { - "CreateIndex": 31, - "Description": "Production API Servers", - "ModifyIndex": 31, - "Name": "api-prod" - }, - { - "CreateIndex": 5, - "Description": "Default shared namespace", - "ModifyIndex": 5, - "Name": "default" - } - ] + {"CreateIndex": 31, "Description": "Production API Servers", "ModifyIndex": 31, "Name": "api-prod"}, + {"CreateIndex": 5, "Description": "Default shared namespace", "ModifyIndex": 5, "Name": "default"}, + ], ) assert "api-prod" in nomad_setup.namespaces @@ -81,19 +56,9 @@ def test_namespaces_len(nomad_setup): "http://{ip}:{port}/v1/namespaces".format(ip=common.IP, port=common.NOMAD_PORT), status=200, json=[ - { - "CreateIndex": 31, - "Description": "Production API Servers", - "ModifyIndex": 31, - "Name": "api-prod" - }, - { - "CreateIndex": 5, - "Description": "Default shared namespace", - "ModifyIndex": 5, - "Name": "default" - } - ] + {"CreateIndex": 31, "Description": "Production API Servers", "ModifyIndex": 31, "Name": "api-prod"}, + {"CreateIndex": 5, "Description": "Default shared namespace", "ModifyIndex": 5, "Name": "default"}, + ], ) assert 2 == len(nomad_setup.namespaces) diff --git a/tests/test_node.py b/tests/test_node.py index 6d02806..055b1f5 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -23,6 +23,9 @@ def test_evaluate_node(nomad_setup): assert "EvalIDs" in nomad_setup.node.evaluate_node(nodeID) +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) > (1, 1, 0), reason="Not supported in version" +) def test_drain_node(nomad_setup): nodeID = nomad_setup.nodes["pynomad1"]["ID"] assert "EvalIDs" in nomad_setup.node.drain_node(nodeID) @@ -32,7 +35,9 @@ def test_drain_node(nomad_setup): assert nomad_setup.node[nodeID]["Drain"] is False -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version" +) def test_drain_node_with_spec(nomad_setup): nodeID = nomad_setup.nodes["pynomad1"]["ID"] assert "EvalIDs" in nomad_setup.node.drain_node_with_spec(nodeID, drain_spec={"Duration": "-100000000"}) @@ -41,7 +46,9 @@ def test_drain_node_with_spec(nomad_setup): assert nomad_setup.node[nodeID]["Drain"] is False -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 8, 1), reason="Not supported in version" +) def test_eligible_node(nomad_setup): nodeID = nomad_setup.nodes["pynomad1"]["ID"] diff --git a/tests/test_nodes.py b/tests/test_nodes.py index f708682..96087e4 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -1,16 +1,30 @@ import pytest +import os # integration tests requires nomad Vagrant VM or Binary running def test_get_nodes(nomad_setup): assert isinstance(nomad_setup.nodes.get_nodes(), list) == True - +def test_get_node(nomad_setup): + node = nomad_setup.nodes.get_nodes()[0] + print(node) + assert node["ID"] in nomad_setup.nodes def test_get_nodes_prefix(nomad_setup): nodes = nomad_setup.nodes.get_nodes() prefix = nodes[0]["ID"][:4] nomad_setup.nodes.get_nodes(prefix=prefix) - +def test_get_nodes_resouces(nomad_setup): + nodes = nomad_setup.nodes.get_nodes(resources=True) + print(nodes) + assert "NodeResources" in nodes[0] + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 3, 0), reason="Not supported in version" +) +def test_get_nodes_os(nomad_setup): + nodes = nomad_setup.nodes.get_nodes(os=True) + assert "os.name" in nodes[0]["Attributes"] def test_dunder_getitem_exist(nomad_setup): n = nomad_setup.nodes["pynomad1"] @@ -46,7 +60,7 @@ def test_dunder_getattr(nomad_setup): def test_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.nodes, '__iter__') + assert hasattr(nomad_setup.nodes, "__iter__") for j in nomad_setup.nodes: pass diff --git a/tests/test_operator.py b/tests/test_operator.py index 85747e4..85d94f3 100644 --- a/tests/test_operator.py +++ b/tests/test_operator.py @@ -4,17 +4,23 @@ # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version" +) def test_get_configuration_default(nomad_setup): assert isinstance(nomad_setup.operator.get_configuration(), dict) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version" +) def test_get_configuration_stale(nomad_setup): assert isinstance(nomad_setup.operator.get_configuration(stale=True), dict) -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 5, 5), reason="Not supported in version" +) def test_delete_peer(nomad_setup): with pytest.raises(exceptions.BaseNomadException): nomad_setup.operator.delete_peer("192.168.33.10:4646") diff --git a/tests/test_regions.py b/tests/test_regions.py index 4a16139..5654419 100644 --- a/tests/test_regions.py +++ b/tests/test_regions.py @@ -44,7 +44,7 @@ def test_dunder_getattr(nomad_setup): def test_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.regions, '__iter__') + assert hasattr(nomad_setup.regions, "__iter__") for j in nomad_setup.regions: pass diff --git a/tests/test_scaling.py b/tests/test_scaling.py new file mode 100644 index 0000000..ffc3ff2 --- /dev/null +++ b/tests/test_scaling.py @@ -0,0 +1,13 @@ +import pytest + +from nomad.api import exceptions + + +def test_scaling_list(nomad_setup): + result = nomad_setup.scaling.get_scaling_policies() + assert not result + + +def test_scaling_policy_not_exist(nomad_setup): + with pytest.raises(exceptions.URLNotFoundNomadException): + nomad_setup.scaling.get_scaling_policy("example") diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..04812b3 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,52 @@ +import os +import pytest + +from nomad.api import exceptions + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search(nomad_setup): + result = nomad_setup.search.search("example", "jobs") + assert "example" in result["Matches"]["jobs"] + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search_incorrect_context(nomad_setup): + # job context doesn't exist + with pytest.raises(exceptions.InvalidParameters): + nomad_setup.search.search("example", "job") + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search_fuzzy(nomad_setup): + result = nomad_setup.search.fuzzy_search("example", "jobs") + assert any(r["ID"] == "example" for r in result["Matches"]["jobs"]) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search_fuzzy_incorrect_context(nomad_setup): + # job context doesn't exist + with pytest.raises(exceptions.InvalidParameters): + nomad_setup.search.fuzzy_search("example", "job") + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search_str(nomad_setup): + assert isinstance(str(nomad_setup.search), str) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 1, 0), reason="Not supported in version" +) +def test_search_repr(nomad_setup): + assert isinstance(repr(nomad_setup.search), str) diff --git a/tests/test_sentinel.py b/tests/test_sentinel.py index 46dc07a..99de2fb 100644 --- a/tests/test_sentinel.py +++ b/tests/test_sentinel.py @@ -19,9 +19,9 @@ def test_list_policies(nomad_setup): "EnforcementLevel": "advisory", "Hash": "CIs8aNX5OfFvo4D7ihWcQSexEJpHp+Za+dHSncVx5+8=", "CreateIndex": 8, - "ModifyIndex": 8 + "ModifyIndex": 8, } - ] + ], ) policies = nomad_setup.sentinel.get_policies() @@ -35,26 +35,22 @@ def test_create_policy(nomad_setup): responses.add( responses.POST, "http://{ip}:{port}/v1/sentinel/policy/my-policy".format(ip=common.IP, port=common.NOMAD_PORT), - status=200 + status=200, ) policy_example = '{"Name": "my-policy", "Description": "This is a great policy", "Scope": "submit-job", "EnforcementLevel": "advisory", "Policy": "main = rule { true }"}' json_policy = json.loads(policy_example) - nomad_setup.sentinel.create_policy(id="my-policy", policy=json_policy) + nomad_setup.sentinel.create_policy(id_="my-policy", policy=json_policy) @responses.activate def test_update_policy(nomad_setup): - responses.add( - responses.POST, - "http://{ip}:{port}/v1/sentinel/policy/my-policy".format(ip=common.IP, port=common.NOMAD_PORT), - status=200 - ) + responses.add(responses.POST, f"http://{common.IP}:{common.NOMAD_PORT}/v1/sentinel/policy/my-policy", status=200) policy_example = '{"Name": "my-policy", "Description": "Update", "Scope": "submit-job", "EnforcementLevel": "advisory", "Policy": "main = rule { true }"}' json_policy = json.loads(policy_example) - nomad_setup.sentinel.update_policy(id="my-policy", policy=json_policy) + nomad_setup.sentinel.update_policy(id_="my-policy", policy=json_policy) @responses.activate @@ -71,8 +67,8 @@ def test_get_policy(nomad_setup): "Policy": "main = rule { true }\n", "Hash": "CIs8aNX5OfFvo4D7ihWcQSexEJpHp+Za+dHSncVx5+8=", "CreateIndex": 8, - "ModifyIndex": 8 - } + "ModifyIndex": 8, + }, ) policy = nomad_setup.sentinel.get_policy("foo") @@ -93,8 +89,8 @@ def test_delete_policy(nomad_setup): "Policy": "main = rule { true }\n", "Hash": "CIs8aNX5OfFvo4D7ihWcQSexEJpHp+Za+dHSncVx5+8=", "CreateIndex": 8, - "ModifyIndex": 8 - } + "ModifyIndex": 8, + }, ) - nomad_setup.sentinel.delete_policy(id="my-policy") + nomad_setup.sentinel.delete_policy(id_="my-policy") diff --git a/tests/test_status.py b/tests/test_status.py index f29494b..dde1a39 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -8,8 +8,7 @@ def test_get_leader(nomad_setup): if int(sys.version[0]) == 3: assert isinstance(nomad_setup.status.leader.get_leader(), str) == True else: - assert isinstance( - nomad_setup.status.leader.get_leader(), unicode) == True + assert isinstance(nomad_setup.status.leader.get_leader(), unicode) == True def test_get_peers(nomad_setup): @@ -35,8 +34,7 @@ def test_peers_dunder_contain_exists(nomad_setup): def test_peers_dunder_contain_not_exist(nomad_setup): - assert "{IP}:4647".format( - IP="172.16.10.100") not in nomad_setup.status.peers + assert "{IP}:4647".format(IP="172.16.10.100") not in nomad_setup.status.peers def test_leader_dunder_contain_exists(nomad_setup): @@ -44,8 +42,7 @@ def test_leader_dunder_contain_exists(nomad_setup): def test_leader_dunder_contain_not_exist(nomad_setup): - assert "{IP}:4647".format( - IP="172.16.10.100") not in nomad_setup.status.leader + assert "{IP}:4647".format(IP="172.16.10.100") not in nomad_setup.status.leader def test_dunder_str(nomad_setup): @@ -67,7 +64,7 @@ def test_dunder_getattr(nomad_setup): def test_peers_dunder_iter(nomad_setup): - assert hasattr(nomad_setup.status.peers, '__iter__') + assert hasattr(nomad_setup.status.peers, "__iter__") for p in nomad_setup.status.peers: pass diff --git a/tests/test_validate.py b/tests/test_validate.py index 1efde82..9888369 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -5,14 +5,18 @@ # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_validate_job(nomad_setup): with open("example.json") as job: nomad_setup.validate.validate_job(json.loads(job.read())) # integration tests requires nomad Vagrant VM or Binary running -@pytest.mark.skipif(tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version") +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (0, 6, 0), reason="Not supported in version" +) def test_invalid_job(nomad_setup): with pytest.raises(nomad.api.exceptions.BadRequestNomadException): nomad_setup.validate.validate_job({}) diff --git a/tests/test_variable.py b/tests/test_variable.py new file mode 100644 index 0000000..a1d1056 --- /dev/null +++ b/tests/test_variable.py @@ -0,0 +1,151 @@ +import pytest +import os + +# Nomad doesn't have any variables by default +from nomad.api import exceptions + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_create_variable(nomad_setup): + payload = { + "Items": {"user": "test", "password": "test123"}, + } + nomad_setup.variable.create_variable("example/first", payload) + assert "example/first" in nomad_setup.variables + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_create_variable_in_namespace(nomad_setup): + payload = { + "Items": {"user": "test2", "password": "321tset"}, + } + nomad_setup.variable.create_variable("example/second", payload, namespace="default") + assert "example/second" in nomad_setup.variables + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_create_variable_with_cas(nomad_setup): + payload = { + "Items": {"user": "test3", "password": "321tset123"}, + } + nomad_setup.variable.create_variable("example/third", payload, cas=0) + assert "example/third" in nomad_setup.variables + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variable_and_check_value(nomad_setup): + var = nomad_setup.variable.get_variable("example/first") + assert var["Items"]["user"] == "test" + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variable_in_namespace(nomad_setup): + var = nomad_setup.variable.get_variable("example/first", namespace="default") + assert var["Items"]["user"] == "test" + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_no_exist_variable(nomad_setup): + with pytest.raises(KeyError): + assert nomad_setup.variable["no_exist"] + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_getitem_exist(nomad_setup): + var = nomad_setup.variable["example/first"] + assert isinstance(var, dict) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_str(nomad_setup): + assert isinstance(str(nomad_setup.variable), str) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_repr(nomad_setup): + assert isinstance(repr(nomad_setup.variable), str) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_getattr(nomad_setup): + with pytest.raises(AttributeError): + nomad_setup.variable.does_not_exist + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_exist(nomad_setup): + assert "example/second" in nomad_setup.variable + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_no_exist(nomad_setup): + assert "no_exist" not in nomad_setup.variable + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variable_getitem_not_exist(nomad_setup): + with pytest.raises(KeyError): + nomad_setup.variable["no_exists"] + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_delete_variable(nomad_setup): + assert 3 == len(nomad_setup.variables.get_variables()) + nomad_setup.variable.delete_variable("example/third") + assert "example/third" not in nomad_setup.variables + assert 2 == len(nomad_setup.variables.get_variables()) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_delete_variable_in_namespace(nomad_setup): + assert 2 == len(nomad_setup.variables.get_variables()) + nomad_setup.variable.delete_variable("example/second", namespace="default") + assert "example/third" not in nomad_setup.variables + assert 1 == len(nomad_setup.variables.get_variables()) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_delete_variable_with_cas(nomad_setup): + variable_path = "variable_with_cas" + payload = { + "Items": {"user": "test4", "password": "test123567"}, + } + var = nomad_setup.variable.create_variable(variable_path, payload) + assert variable_path in nomad_setup.variables + with pytest.raises(exceptions.VariableConflict): + nomad_setup.variable.delete_variable(variable_path, cas=var["ModifyIndex"] + 1) + assert variable_path in nomad_setup.variables + nomad_setup.variable.delete_variable(variable_path, cas=var["ModifyIndex"]) + assert variable_path not in nomad_setup.variables diff --git a/tests/test_variables.py b/tests/test_variables.py new file mode 100644 index 0000000..c39d31b --- /dev/null +++ b/tests/test_variables.py @@ -0,0 +1,91 @@ +import pytest +import os + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variables(nomad_setup): + assert 1 == len(nomad_setup.variables.get_variables()) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variables_with_prefix(nomad_setup): + assert 1 == len(nomad_setup.variables.get_variables("example/first")) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variables_with_prefix_no_exist(nomad_setup): + assert 0 == len(nomad_setup.variables.get_variables("no_exist_var")) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_get_variables_from_namespace(nomad_setup): + assert 1 == len(nomad_setup.variables.get_variables(namespace="default")) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_iter_variables(nomad_setup): + assert hasattr(nomad_setup.variables, "__iter__") + for _ in nomad_setup.variables: + pass + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_str(nomad_setup): + assert isinstance(str(nomad_setup.variables), str) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_repr(nomad_setup): + assert isinstance(repr(nomad_setup.variables), str) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_not_exist(nomad_setup): + assert "no_exist" not in nomad_setup.variables + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_exist(nomad_setup): + assert "example/first" in nomad_setup.variables + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_getitem_exist(nomad_setup): + var = nomad_setup.variables["example/first"] + assert isinstance(var, dict) + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_getitem_not_exist(nomad_setup): + with pytest.raises(KeyError): + nomad_setup.variables["no_exists"] + + +@pytest.mark.skipif( + tuple(int(i) for i in os.environ.get("NOMAD_VERSION").split(".")) < (1, 4, 0), reason="Not supported in version" +) +def test_variables_getattr(nomad_setup): + with pytest.raises(AttributeError): + nomad_setup.variables.does_not_exist