├── blazarclient ├── __init__.py ├── osc │ ├── __init__.py │ └── plugin.py ├── v1 │ ├── __init__.py │ ├── shell_commands │ │ ├── __init__.py │ │ ├── floatingips.py │ │ ├── allocations.py │ │ ├── hosts.py │ │ └── leases.py │ ├── allocations.py │ ├── floatingips.py │ ├── client.py │ ├── hosts.py │ └── leases.py ├── tests │ ├── v1 │ │ ├── __init__.py │ │ └── shell_commands │ │ │ ├── __init__.py │ │ │ ├── test_floatingips.py │ │ │ ├── test_hosts.py │ │ │ └── test_leases.py │ ├── __init__.py │ ├── test_plugin.py │ ├── test_client.py │ ├── test_shell.py │ ├── test_command.py │ └── test_base.py ├── version.py ├── i18n.py ├── client.py ├── exception.py ├── base.py ├── utils.py ├── command.py └── shell.py ├── releasenotes ├── notes │ ├── .placeholder │ ├── drop-python2-c3c1601e92a9b87a.yaml │ ├── flavor-based-instance-reservation-ec9730ddfeabdf15.yaml │ ├── openstackclient-support-f591eef2dc3c1a8b.yaml │ ├── drop-python-3-8-and-3-9-51d2e25d71240731.yaml │ ├── default-affinity-value-150947560fd7da3c.yaml │ ├── host-resource-property-9ac5c21bd3ca6699.yaml │ ├── respect-selected-region-a409773f851ccb47.yaml │ ├── separate-id-parser-argument-from-showcommand-ce92d0c52dd1963e.yaml │ ├── floatingip-reservation-update-5823a21516135f17.yaml │ ├── floatingip-support-d184a565f324d31b.yaml │ ├── host-allocation-fad8e511cb13c0e8.yaml │ ├── parse-required-floatingips-f79f79d652e371ae.yaml │ ├── bug-1777548-6b5c770abc6ac360.yaml │ ├── bug-1783296-set-start-date-to-now-e329a6923c11432f.yaml │ └── ksa-loading-9731c570772c826a.yaml └── source │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── unreleased.rst │ ├── zed.rst │ ├── train.rst │ ├── xena.rst │ ├── yoga.rst │ ├── 2023.2.rst │ ├── 2024.2.rst │ ├── 2025.1.rst │ ├── 2025.2.rst │ ├── ussuri.rst │ ├── 2023.1.rst │ ├── 2024.1.rst │ ├── rocky.rst │ ├── stein.rst │ ├── wallaby.rst │ ├── victoria.rst │ ├── index.rst │ └── conf.py ├── .stestr.conf ├── .gitreview ├── .zuul.yaml ├── MANIFEST.in ├── .gitignore ├── HACKING.rst ├── doc └── requirements.txt ├── test-requirements.txt ├── requirements.txt ├── setup.py ├── README.rst ├── tox.ini ├── setup.cfg └── LICENSE /blazarclient/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blazarclient/osc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blazarclient/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blazarclient/tests/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/notes/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blazarclient/v1/shell_commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/source/_templates/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blazarclient/tests/v1/shell_commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./blazarclient/tests/ 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/python-blazarclient.git 5 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python2-c3c1601e92a9b87a.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 2 is no longer supported. Python 3 is required. 5 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Current Series Release Notes 3 | ============================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /releasenotes/notes/flavor-based-instance-reservation-ec9730ddfeabdf15.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Add support for creating flavor-based instance reservations. 5 | -------------------------------------------------------------------------------- /releasenotes/source/zed.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Zed Series Release Notes 3 | ======================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/zed 7 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - check-requirements 4 | - openstack-python3-jobs 5 | - release-notes-jobs-python3 6 | - openstack-cover-jobs 7 | -------------------------------------------------------------------------------- /releasenotes/source/train.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Train Series Release Notes 3 | ========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/train 7 | -------------------------------------------------------------------------------- /releasenotes/source/xena.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Xena Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/xena 7 | -------------------------------------------------------------------------------- /releasenotes/source/yoga.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Yoga Series Release Notes 3 | ========================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/yoga 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README.rst 3 | include ChangeLog 4 | include LICENSE 5 | 6 | exclude .gitignore 7 | exclude .gitreview 8 | 9 | global-exclude *.pyc 10 | -------------------------------------------------------------------------------- /releasenotes/source/2023.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2023.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2024.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/2025.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2025.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2025.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2025.2.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2025.2 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/2025.2 7 | -------------------------------------------------------------------------------- /releasenotes/source/ussuri.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Ussuri Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: stable/ussuri 7 | -------------------------------------------------------------------------------- /releasenotes/source/2023.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2023.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/2023.1 7 | -------------------------------------------------------------------------------- /releasenotes/source/2024.1.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | 2024.1 Series Release Notes 3 | =========================== 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/2024.1 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg 3 | *.egg-info 4 | dist 5 | build 6 | eggs 7 | 8 | .tox 9 | .venv 10 | .idea 11 | 12 | AUTHORS 13 | ChangeLog 14 | .stestr/ 15 | cover/ 16 | .coverage 17 | -------------------------------------------------------------------------------- /releasenotes/source/rocky.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Rocky Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/rocky 7 | -------------------------------------------------------------------------------- /releasenotes/source/stein.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Stein Series Release Notes 3 | =================================== 4 | 5 | .. release-notes:: 6 | :branch: stable/stein 7 | -------------------------------------------------------------------------------- /releasenotes/source/wallaby.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Wallaby Series Release Notes 3 | ============================ 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/wallaby 7 | -------------------------------------------------------------------------------- /releasenotes/source/victoria.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Victoria Series Release Notes 3 | ============================= 4 | 5 | .. release-notes:: 6 | :branch: unmaintained/victoria 7 | -------------------------------------------------------------------------------- /releasenotes/notes/openstackclient-support-f591eef2dc3c1a8b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Add openstackclient plugin support, enabling blazar commands to be used 5 | within the openstack CLI. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python-3-8-and-3-9-51d2e25d71240731.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 3.8 and 3.9 support has been dropped. The minimum version of Python 5 | now supported is Python 3.10. 6 | -------------------------------------------------------------------------------- /releasenotes/notes/default-affinity-value-150947560fd7da3c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Fixes creation of instance reservations when no affinity value is provided, 5 | in which case affinity is set to ``None``. 6 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | Blazar Style Commandments 2 | ========================= 3 | 4 | - Step 1: Read the OpenStack Style Commandments 5 | https://docs.openstack.org/hacking/latest/ 6 | - Step 2: Read on 7 | 8 | Blazar Specific Commandments 9 | ---------------------------- 10 | 11 | None so far 12 | 13 | -------------------------------------------------------------------------------- /releasenotes/notes/host-resource-property-9ac5c21bd3ca6699.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Added support for managing host resource properties using the following new 5 | commands: 6 | 7 | * ``host-property-list`` 8 | * ``host-property-show`` 9 | * ``host-property-set`` 10 | -------------------------------------------------------------------------------- /releasenotes/notes/respect-selected-region-a409773f851ccb47.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | The region name value provided via an environment variable or a command 5 | line argument is now respected by the client. Without this fix, the wrong 6 | reservation endpoint could be selected in a multi-region cloud. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/separate-id-parser-argument-from-showcommand-ce92d0c52dd1963e.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | other: 3 | - | 4 | The ID parser argument is moved from the ShowCommand class to each resource 5 | class. This allows the ordering of arguments to be fully customised, 6 | instead of requiring the ID to come first. 7 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | 5 | openstackdocstheme>=2.2.1 # Apache-2.0 6 | reno>=3.1.0 # Apache-2.0 7 | sphinx>=2.0.0,!=2.1.0 # BSD 8 | -------------------------------------------------------------------------------- /releasenotes/notes/floatingip-reservation-update-5823a21516135f17.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | The command-line client now parses floating IP reservation values when 5 | using the ``lease-update`` command. Note that while accepted by the client, 6 | the Blazar service may prevent the update of some floating IP reservation 7 | values. 8 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Blazar Client Release Notes 3 | ============================= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | 2025.2 10 | 2025.1 11 | 2024.2 12 | 2024.1 13 | 2023.2 14 | 2023.1 15 | zed 16 | yoga 17 | xena 18 | wallaby 19 | victoria 20 | ussuri 21 | train 22 | stein 23 | rocky 24 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | hacking>=7.0.0,<7.1.0 # Apache-2.0 5 | 6 | oslotest>=3.2.0 # Apache-2.0 7 | fixtures>=3.0.0 # Apache-2.0/BSD 8 | stestr>=2.0.0 # Apache-2.0 9 | testtools>=2.2.0 # MIT 10 | coverage!=4.4,>=4.0 # Apache-2.0 11 | -------------------------------------------------------------------------------- /releasenotes/notes/floatingip-support-d184a565f324d31b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Added support for operators to manage reservable floating IPs using the 5 | following new commands: 6 | 7 | * ``floatingip-create`` 8 | * ``floatingip-delete`` 9 | * ``floatingip-list`` 10 | * ``floatingip-show`` 11 | - | 12 | Added support for users to create floating IP reservations using the 13 | ``virtual:floatingip`` resource type. 14 | -------------------------------------------------------------------------------- /releasenotes/notes/host-allocation-fad8e511cb13c0e8.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Adds support for querying the Resource Allocation API with the following 5 | commands: 6 | 7 | * ``blazar allocation-list`` 8 | * ``blazar allocation-show`` 9 | 10 | Only the ``host`` resource type is currently supported by the Blazar 11 | service. 12 | 13 | OpenStackClient commands are also available: 14 | 15 | * ``openstack reservation allocation list`` 16 | * ``openstack reservation allocation show`` 17 | -------------------------------------------------------------------------------- /releasenotes/notes/parse-required-floatingips-f79f79d652e371ae.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Parse the ``required_floatingips`` command-line parameter as a list instead 5 | of a string, to pass it to the API in the expected format. For example, 6 | this parameter can be used in the following fashion: 7 | 8 | ``blazar lease-create --reservation 'resource_type=virtual:floatingip,network_id=81fabec7-00ae-497a-b485-72f4bf187d3e,amount=2,required_floatingips=["172.24.4.2","172.24.4.3"]' fip-lease`` 9 | 10 | For more details, see `bug 1843258 `_. 11 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1777548-6b5c770abc6ac360.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | When the blazar CLI client got an error code from the blazar server, 5 | it didn't display error messages created in the blazar server. 6 | Instead, it displayed `messages created in keystoneauth`_ with poor 7 | information. See the `bug report`_ for example. 8 | It was fixed to display original error messages which include useful 9 | information. 10 | 11 | .. _messages created in keystoneauth: https://github.com/openstack/keystoneauth/blob/master/keystoneauth1/exceptions/http.py 12 | .. _bug report: https://bugs.launchpad.net/blazar/+bug/1777548 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements lower bounds listed here are our best effort to keep them up to 2 | # date but we do not test them so no guarantee of having them all correct. If 3 | # you find any incorrect lower bounds, let us know or propose a fix. 4 | 5 | # The order of packages is significant, because pip processes them in the order 6 | # of appearance. Changing the order has an impact on the overall integration 7 | # process, which may cause wedges in the gate later. 8 | pbr!=2.1.0,>=2.0.0 # Apache-2.0 9 | cliff!=2.9.0,>=2.8.0 # Apache-2.0 10 | PrettyTable>=0.7.1 # BSD 11 | oslo.i18n>=3.15.3 # Apache-2.0 12 | oslo.log>=3.36.0 # Apache-2.0 13 | oslo.utils>=7.0.0 # Apache-2.0 14 | keystoneauth1>=3.4.0 # Apache-2.0 15 | osc-lib>=1.3.0 # Apache-2.0 16 | -------------------------------------------------------------------------------- /blazarclient/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import pbr.version 17 | 18 | 19 | __version__ = pbr.version.VersionInfo('python-blazarclient').version_string() 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | setuptools.setup( 19 | setup_requires=['pbr>=2.0.0'], 20 | pbr=True) 21 | -------------------------------------------------------------------------------- /blazarclient/i18n.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 IBM Corp. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """oslo.i18n integration module. 16 | 17 | See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . 18 | 19 | """ 20 | 21 | import oslo_i18n 22 | 23 | 24 | _translators = oslo_i18n.TranslatorFactory(domain='blazarclient') 25 | 26 | # The primary translation function using the well-known name "_" 27 | _ = _translators.primary 28 | -------------------------------------------------------------------------------- /releasenotes/notes/bug-1783296-set-start-date-to-now-e329a6923c11432f.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | When creating a lease using the CLI client, the default value for start 5 | date was changed to use the string 'now', which is resolved to the current 6 | time on the server rather than on the client. Note that if the request is 7 | sent at the end of a minute and interpreted by the service at the beginning of 8 | the next minute, this can result in leases that are one minute shorter than 9 | what the user might expect, as the end date is still specified by the 10 | client. Users who care about the exact timing of their leases should 11 | explicitly specify both start and end dates. 12 | fixes: 13 | - | 14 | Creating a lease using the CLI client without specifying a start date no 15 | longer fails if the request is sent to the Blazar service just before the 16 | end of a minute. For more details, see `bug 1783296 17 | `_. 18 | -------------------------------------------------------------------------------- /blazarclient/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) 2014 Mirantis. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import fixtures 18 | from oslotest import base 19 | 20 | 21 | class TestCase(base.BaseTestCase): 22 | """Test case base class for all unit tests.""" 23 | 24 | def patch(self, obj, attr): 25 | """Returns a Mocked object on the patched attribute.""" 26 | mockfixture = self.useFixture(fixtures.MockPatchObject(obj, attr)) 27 | return mockfixture.mock 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Team and repository tags 3 | ======================== 4 | 5 | .. image:: https://governance.openstack.org/tc/badges/python-blazarclient.svg 6 | :target: https://governance.openstack.org/tc/reference/tags/index.html 7 | 8 | .. Change things from this point on 9 | 10 | ============= 11 | Blazar client 12 | ============= 13 | 14 | This is a client for the OpenStack Blazar API. It provides a Python API (the 15 | **blazarclient** module) and a command-line script (**blazar**). 16 | 17 | Other Resources 18 | --------------- 19 | 20 | * Source code: 21 | 22 | * `Blazar `__ 23 | * `Nova scheduler filter `__ 24 | * `Client tools `__ 25 | * `Dashboard (Horizon plugin) `__ 26 | 27 | * Blueprints/Bugs: https://launchpad.net/blazar 28 | * Documentation: https://docs.openstack.org/blazar/latest/ 29 | * Release notes: https://docs.openstack.org/releasenotes/python-blazarclient/ 30 | -------------------------------------------------------------------------------- /releasenotes/notes/ksa-loading-9731c570772c826a.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | deprecations: 3 | - | 4 | The ``blazar`` command-line client has switched to the 5 | ``keystoneauth1.loading`` module. As a result, the following options are 6 | deprecated: 7 | 8 | * ``--service-type`` (use ``--os-service-type`` instead) 9 | * ``--endpoint-type`` (use ``--os-interface`` instead) 10 | 11 | The following options have been removed: 12 | 13 | * ``--os-auth-strategy`` (this option had not effect) 14 | * ``--os_auth_strategy`` (this option had not effect) 15 | * ``--os_auth_url`` (use ``--os-auth-url`` instead) 16 | * ``--os_project_name`` (use ``--os-project-name`` instead) 17 | * ``--os_project_id`` (use ``--os-project-id`` instead) 18 | * ``--os_project_domain_name`` (use ``--os-project-domain-name`` instead) 19 | * ``--os_project_domain_id`` (use ``--os-project-domain-id`` instead) 20 | * ``--os_tenant_name`` (use ``--os-project-name`` or ``--os-tenant-name`` instead) 21 | * ``--os_username`` (use ``--os-username`` instead) 22 | * ``--os_user_domain_name`` (use ``--os-user-domain-name`` instead) 23 | * ``--os_user_domain_id`` (use ``--os-user-domain-id`` instead) 24 | * ``--os_token`` (use ``--os-token`` instead) 25 | -------------------------------------------------------------------------------- /blazarclient/tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | from unittest import mock 14 | 15 | from blazarclient.osc import plugin 16 | from blazarclient import tests 17 | 18 | 19 | class ReservationPluginTests(tests.TestCase): 20 | 21 | @mock.patch("blazarclient.v1.client.Client") 22 | def test_make_client(self, mock_client): 23 | instance = mock.Mock() 24 | instance._api_version = {"reservation": "1"} 25 | endpoint = "blazar_endpoint" 26 | instance.get_endpoint_for_service_type = mock.Mock( 27 | return_value=endpoint 28 | ) 29 | 30 | plugin.make_client(instance) 31 | 32 | mock_client.assert_called_with( 33 | "1", 34 | session=instance.session, 35 | endpoint_override=endpoint 36 | ) 37 | -------------------------------------------------------------------------------- /blazarclient/v1/allocations.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 University of Chicago. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from blazarclient import base 17 | 18 | 19 | class AllocationClientManager(base.BaseClientManager): 20 | """Manager for the ComputeHost connected requests.""" 21 | 22 | def get(self, resource, resource_id): 23 | """Get allocation for resource identified by type and ID.""" 24 | resp, body = self.request_manager.get( 25 | '/%s/%s/allocation' % (resource, resource_id)) 26 | return body['allocation'] 27 | 28 | def list(self, resource, sort_by=None): 29 | """List allocations for all resources of a type.""" 30 | resp, body = self.request_manager.get('/%s/allocations' % resource) 31 | allocations = body['allocations'] 32 | if sort_by: 33 | allocations = sorted(allocations, key=lambda alloc: alloc[sort_by]) 34 | return allocations 35 | -------------------------------------------------------------------------------- /blazarclient/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from oslo_utils import importutils 17 | 18 | from blazarclient import exception 19 | from blazarclient.i18n import _ 20 | 21 | 22 | def Client(version=1, service_type='reservation', *args, **kwargs): 23 | version_map = { 24 | '1': 'blazarclient.v1.client.Client', 25 | '1a0': 'blazarclient.v1.client.Client', 26 | } 27 | try: 28 | client_path = version_map[str(version)] 29 | except (KeyError, ValueError): 30 | msg = _("Invalid client version '%(version)s'. " 31 | "Must be one of: %(available_version)s") % ({ 32 | 'version': version, 33 | 'available_version': ', '.join(version_map.keys()) 34 | }) 35 | raise exception.UnsupportedVersion(msg) 36 | 37 | return importutils.import_object(client_path, 38 | service_type=service_type, 39 | *args, **kwargs) 40 | -------------------------------------------------------------------------------- /blazarclient/tests/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from oslo_utils import importutils 17 | 18 | from blazarclient import client 19 | from blazarclient import exception 20 | from blazarclient import tests 21 | 22 | 23 | class BaseClientTestCase(tests.TestCase): 24 | 25 | def setUp(self): 26 | super(BaseClientTestCase, self).setUp() 27 | 28 | self.client = client 29 | 30 | self.import_obj = self.patch(importutils, "import_object") 31 | 32 | def test_with_v1(self): 33 | self.client.Client() 34 | self.import_obj.assert_called_once_with( 35 | 'blazarclient.v1.client.Client', 36 | service_type='reservation') 37 | 38 | def test_with_v1a0(self): 39 | self.client.Client(version='1a0') 40 | self.import_obj.assert_called_once_with( 41 | 'blazarclient.v1.client.Client', 42 | service_type='reservation') 43 | 44 | def test_with_wrong_vers(self): 45 | self.assertRaises(exception.UnsupportedVersion, 46 | self.client.Client, 47 | version='0.0') 48 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18.0 3 | envlist = py3,pep8 4 | ignore_basepython_conflict = True 5 | 6 | [testenv] 7 | basepython = python3 8 | install_command = pip install {opts} {packages} 9 | deps = 10 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 11 | -r{toxinidir}/test-requirements.txt 12 | -r{toxinidir}/requirements.txt 13 | setenv = VIRTUAL_ENV={envdir} 14 | DISCOVER_DIRECTORY=blazarclient/tests 15 | commands = stestr run --slowest '{posargs}' 16 | 17 | [testenv:pep8] 18 | commands = flake8 19 | 20 | [flake8] 21 | show-source = true 22 | builtins = _ 23 | # Ignore currently failing tests for now 24 | # W504 skipped because it is overeager and unnecessary 25 | ignore = E265,H405,W504 26 | exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg 27 | 28 | [hacking] 29 | import_exceptions = blazarclient.i18n 30 | 31 | [testenv:venv] 32 | deps = 33 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 34 | -r{toxinidir}/requirements.txt 35 | -r{toxinidir}/doc/requirements.txt 36 | commands = {posargs} 37 | 38 | [testenv:cover] 39 | allowlist_externals = find 40 | setenv = 41 | {[testenv]setenv} 42 | PYTHON=coverage run --source blazarclient --parallel-mode 43 | commands = 44 | coverage erase 45 | find . -type f -name "*.pyc" -delete 46 | stestr run {posargs} 47 | coverage combine 48 | coverage html -d cover 49 | coverage xml -o cover/coverage.xml 50 | coverage report 51 | 52 | [testenv:releasenotes] 53 | deps = 54 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 55 | -r{toxinidir}/doc/requirements.txt 56 | commands = sphinx-build -a -W -E -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html 57 | -------------------------------------------------------------------------------- /blazarclient/v1/floatingips.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 StackHPC Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from blazarclient import base 17 | 18 | 19 | class FloatingIPClientManager(base.BaseClientManager): 20 | """Manager for floating IP requests.""" 21 | 22 | def create(self, network_id, floating_ip_address, **kwargs): 23 | """Creates a floating IP from values passed.""" 24 | values = {'floating_network_id': network_id, 25 | 'floating_ip_address': floating_ip_address} 26 | values.update(**kwargs) 27 | resp, body = self.request_manager.post('/floatingips', body=values) 28 | return body['floatingip'] 29 | 30 | def get(self, floatingip_id): 31 | """Show floating IP details.""" 32 | resp, body = self.request_manager.get( 33 | '/floatingips/%s' % floatingip_id) 34 | return body['floatingip'] 35 | 36 | def delete(self, floatingip_id): 37 | """Deletes floating IP with specified ID.""" 38 | resp, body = self.request_manager.delete( 39 | '/floatingips/%s' % floatingip_id) 40 | 41 | def list(self, sort_by=None): 42 | """List all floating IPs.""" 43 | resp, body = self.request_manager.get('/floatingips') 44 | floatingips = body['floatingips'] 45 | if sort_by: 46 | floatingips = sorted(floatingips, key=lambda fip: fip[sort_by]) 47 | return floatingips 48 | -------------------------------------------------------------------------------- /blazarclient/osc/plugin.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | import logging 14 | 15 | from osc_lib import utils 16 | 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | DEFAULT_API_VERSION = '1' 21 | 22 | # Required by the OSC plugin interface 23 | API_NAME = 'reservation' 24 | API_VERSION_OPTION = 'os_reservations_api_version' 25 | API_VERSIONS = { 26 | '1': 'blazarclient.v1.client.Client', 27 | } 28 | 29 | 30 | # Required by the OSC plugin interface 31 | def make_client(instance): 32 | reservation_client = utils.get_client_class( 33 | API_NAME, 34 | instance._api_version[API_NAME], 35 | API_VERSIONS) 36 | 37 | LOG.debug("Instantiating reservation client: %s", reservation_client) 38 | 39 | client = reservation_client( 40 | instance._api_version[API_NAME], 41 | session=instance.session, 42 | endpoint_override=instance.get_endpoint_for_service_type( 43 | API_NAME, 44 | interface=instance.interface, 45 | region_name=instance._region_name) 46 | ) 47 | return client 48 | 49 | 50 | # Required by the OSC plugin interface 51 | def build_option_parser(parser): 52 | """Hook to add global options. 53 | Called from openstackclient.shell.OpenStackShell.__init__() 54 | after the builtin parser has been initialized. This is 55 | where a plugin can add global options such as an API version setting. 56 | :param argparse.ArgumentParser parser: The parser object that has been 57 | initialized by OpenStackShell. 58 | """ 59 | parser.add_argument( 60 | "--os-reservation-api-version", 61 | metavar="", 62 | help="Reservation API version, default=" 63 | "{} (Env: OS_RESERVATION_API_VERSION)".format( 64 | DEFAULT_API_VERSION) 65 | ) 66 | return parser 67 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = python-blazarclient 3 | summary = Client for OpenStack Reservation Service 4 | description_file = README.rst 5 | license = Apache Software License 6 | python_requires = >=3.10 7 | classifiers = 8 | Programming Language :: Python 9 | Programming Language :: Python :: Implementation :: CPython 10 | Programming Language :: Python :: 3 :: Only 11 | Programming Language :: Python :: 3 12 | Programming Language :: Python :: 3.10 13 | Programming Language :: Python :: 3.11 14 | Programming Language :: Python :: 3.12 15 | Environment :: OpenStack 16 | Development Status :: 3 - Alpha 17 | Framework :: Setuptools Plugin 18 | Intended Audience :: Information Technology 19 | Intended Audience :: System Administrators 20 | License :: OSI Approved :: Apache Software License 21 | Operating System :: POSIX :: Linux 22 | author = OpenStack 23 | author_email = openstack-discuss@lists.openstack.org 24 | home_page = https://launchpad.net/blazar 25 | 26 | [files] 27 | packages = 28 | blazarclient 29 | 30 | [entry_points] 31 | console_scripts = 32 | blazar = blazarclient.shell:main 33 | 34 | openstack.cli.extension = 35 | reservation = blazarclient.osc.plugin 36 | 37 | openstack.reservation.v1 = 38 | reservation_allocation_list = blazarclient.v1.shell_commands.allocations:ListAllocations 39 | reservation_allocation_show = blazarclient.v1.shell_commands.allocations:ShowAllocations 40 | reservation_floatingip_create = blazarclient.v1.shell_commands.floatingips:CreateFloatingIP 41 | reservation_floatingip_delete = blazarclient.v1.shell_commands.floatingips:DeleteFloatingIP 42 | reservation_floatingip_list = blazarclient.v1.shell_commands.floatingips:ListFloatingIPs 43 | reservation_floatingip_show = blazarclient.v1.shell_commands.floatingips:ShowFloatingIP 44 | reservation_host_create = blazarclient.v1.shell_commands.hosts:CreateHost 45 | reservation_host_delete = blazarclient.v1.shell_commands.hosts:DeleteHost 46 | reservation_host_list = blazarclient.v1.shell_commands.hosts:ListHosts 47 | reservation_host_property_list = blazarclient.v1.shell_commands.hosts:ListHostProperties 48 | reservation_host_property_set = blazarclient.v1.shell_commands.hosts:UpdateHostProperty 49 | reservation_host_property_show = blazarclient.v1.shell_commands.hosts:ShowHostProperty 50 | reservation_host_set = blazarclient.v1.shell_commands.hosts:UpdateHost 51 | reservation_host_show = blazarclient.v1.shell_commands.hosts:ShowHost 52 | reservation_lease_create = blazarclient.v1.shell_commands.leases:CreateLeaseBase 53 | reservation_lease_delete = blazarclient.v1.shell_commands.leases:DeleteLease 54 | reservation_lease_list = blazarclient.v1.shell_commands.leases:ListLeases 55 | reservation_lease_set = blazarclient.v1.shell_commands.leases:UpdateLease 56 | reservation_lease_show = blazarclient.v1.shell_commands.leases:ShowLease 57 | -------------------------------------------------------------------------------- /blazarclient/v1/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | 18 | from blazarclient.v1 import allocations 19 | from blazarclient.v1 import floatingips 20 | from blazarclient.v1 import hosts 21 | from blazarclient.v1 import leases 22 | 23 | 24 | class Client(object): 25 | """Top level object to communicate with Blazar. 26 | 27 | Contains managers to control requests that should be passed to each type of 28 | resources - leases, events, etc. 29 | 30 | **Examples** 31 | client = Client() 32 | client.lease.list() 33 | client.event.list() 34 | ... 35 | """ 36 | 37 | version = '1' 38 | 39 | def __init__(self, blazar_url=None, auth_token=None, session=None, 40 | **kwargs): 41 | self.blazar_url = blazar_url 42 | self.auth_token = auth_token 43 | self.session = session 44 | 45 | if not self.session: 46 | logging.warning('Use a keystoneauth session object for the ' 47 | 'authentication. The authentication with ' 48 | 'blazar_url and auth_token is deprecated.') 49 | 50 | self.lease = leases.LeaseClientManager(blazar_url=self.blazar_url, 51 | auth_token=self.auth_token, 52 | session=self.session, 53 | version=self.version, 54 | **kwargs) 55 | self.host = hosts.ComputeHostClientManager(blazar_url=self.blazar_url, 56 | auth_token=self.auth_token, 57 | session=self.session, 58 | version=self.version, 59 | **kwargs) 60 | self.floatingip = floatingips.FloatingIPClientManager( 61 | blazar_url=self.blazar_url, 62 | auth_token=self.auth_token, 63 | session=self.session, 64 | version=self.version, 65 | **kwargs) 66 | self.allocation = allocations.AllocationClientManager( 67 | blazar_url=self.blazar_url, 68 | auth_token=self.auth_token, 69 | session=self.session, 70 | version=self.version, 71 | **kwargs) 72 | -------------------------------------------------------------------------------- /blazarclient/v1/shell_commands/floatingips.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 StackHPC Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | 18 | from blazarclient import command 19 | 20 | 21 | class ListFloatingIPs(command.ListCommand): 22 | """Print a list of floating IPs.""" 23 | resource = 'floatingip' 24 | log = logging.getLogger(__name__ + '.ListFloatingIPs') 25 | list_columns = ['id', 'floating_ip_address', 'floating_network_id'] 26 | 27 | def get_parser(self, prog_name): 28 | parser = super(ListFloatingIPs, self).get_parser(prog_name) 29 | parser.add_argument( 30 | '--sort-by', metavar="", 31 | help='column name used to sort result', 32 | default='id' 33 | ) 34 | return parser 35 | 36 | 37 | class ShowFloatingIP(command.ShowCommand): 38 | """Show floating IP details.""" 39 | resource = 'floatingip' 40 | allow_names = False 41 | json_indent = 4 42 | log = logging.getLogger(__name__ + '.ShowFloatingIP') 43 | 44 | def get_parser(self, prog_name): 45 | parser = super(ShowFloatingIP, self).get_parser(prog_name) 46 | if self.allow_names: 47 | help_str = 'ID or name of %s to look up' 48 | else: 49 | help_str = 'ID of %s to look up' 50 | parser.add_argument('id', metavar=self.resource.upper(), 51 | help=help_str % self.resource) 52 | return parser 53 | 54 | 55 | class CreateFloatingIP(command.CreateCommand): 56 | """Create a floating IP.""" 57 | resource = 'floatingip' 58 | json_indent = 4 59 | log = logging.getLogger(__name__ + '.CreateFloatingIP') 60 | 61 | def get_parser(self, prog_name): 62 | parser = super(CreateFloatingIP, self).get_parser(prog_name) 63 | parser.add_argument( 64 | 'network_id', metavar='NETWORK_ID', 65 | help='External network ID to which the floating IP belongs' 66 | ) 67 | parser.add_argument( 68 | 'floating_ip_address', metavar='FLOATING_IP_ADDRESS', 69 | help='Floating IP address to add to Blazar' 70 | ) 71 | return parser 72 | 73 | def args2body(self, parsed_args): 74 | params = {} 75 | if parsed_args.network_id: 76 | params['network_id'] = parsed_args.network_id 77 | if parsed_args.floating_ip_address: 78 | params['floating_ip_address'] = parsed_args.floating_ip_address 79 | return params 80 | 81 | 82 | class DeleteFloatingIP(command.DeleteCommand): 83 | """Delete a floating IP.""" 84 | resource = 'floatingip' 85 | allow_names = False 86 | log = logging.getLogger(__name__ + '.DeleteFloatingIP') 87 | -------------------------------------------------------------------------------- /blazarclient/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | from blazarclient.i18n import _ 18 | 19 | 20 | class BlazarClientException(Exception): 21 | """Base exception class.""" 22 | message = _("An unknown exception occurred %s.") 23 | code = 500 24 | 25 | def __init__(self, message=None, **kwargs): 26 | self.kwargs = kwargs 27 | 28 | if 'code' not in self.kwargs: 29 | try: 30 | self.kwargs['code'] = self.code 31 | except AttributeError: 32 | pass 33 | 34 | if not message: 35 | message = self.message % kwargs 36 | 37 | super(BlazarClientException, self).__init__(message) 38 | 39 | 40 | class CommandError(BlazarClientException): 41 | """Occurs if not all authentication vital options are set.""" 42 | message = _("You have to provide all options like user name or tenant " 43 | "id to make authentication possible.") 44 | code = 401 45 | 46 | 47 | class NotAuthorized(BlazarClientException): 48 | """HTTP 401 - Not authorized. 49 | 50 | User have no enough rights to perform action. 51 | """ 52 | code = 401 53 | message = _("Not authorized request.") 54 | 55 | 56 | class NoBlazarEndpoint(BlazarClientException): 57 | """Occurs if no endpoint for Blazar set in the Keystone.""" 58 | message = _("No publicURL endpoint for Blazar found. Set endpoint " 59 | "for Blazar in the Keystone.") 60 | code = 404 61 | 62 | 63 | class NoUniqueMatch(BlazarClientException): 64 | """Occurs if there are more than one appropriate resources.""" 65 | message = _("There is no unique requested resource.") 66 | code = 409 67 | 68 | 69 | class UnsupportedVersion(BlazarClientException): 70 | """Occurs if unsupported client version was requested.""" 71 | message = _("Unsupported client version requested.") 72 | code = 406 73 | 74 | 75 | class IncorrectLease(BlazarClientException): 76 | """Occurs if lease parameters are incorrect.""" 77 | message = _("The lease parameters are incorrect.") 78 | code = 409 79 | 80 | 81 | class DuplicatedLeaseParameters(BlazarClientException): 82 | """Occurs if lease parameters are duplicated.""" 83 | message = _("The lease parameters are duplicated.") 84 | code = 400 85 | 86 | 87 | class InsufficientAuthInformation(BlazarClientException): 88 | """Occurs if the auth info passed to blazar client is insufficient.""" 89 | message = _("The passed arguments are insufficient " 90 | "for the authentication. The instance of " 91 | "keystoneauth1.session.Session class is required.") 92 | code = 400 93 | 94 | 95 | class ResourcePropertyNotFound(BlazarClientException): 96 | """Occurs if the resource property specified does not exist""" 97 | message = _("The resource property does not exist.") 98 | code = 404 99 | -------------------------------------------------------------------------------- /blazarclient/tests/test_shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import io 17 | import re 18 | import sys 19 | 20 | import fixtures 21 | #note(n.s.): you may need it later 22 | #import mock 23 | import testtools 24 | 25 | #note(n.s.): you may need it later 26 | #from blazarclient import client as blazar_client 27 | #from blazarclient import exception 28 | from blazarclient import shell 29 | from blazarclient import tests 30 | 31 | FAKE_ENV = {'OS_USERNAME': 'username', 32 | 'OS_USER_DOMAIN_ID': 'user_domain_id', 33 | 'OS_PASSWORD': 'password', 34 | 'OS_TENANT_NAME': 'tenant_name', 35 | 'OS_PROJECT_NAME': 'project_name', 36 | 'OS_PROJECT_DOMAIN_ID': 'project_domain_id', 37 | 'OS_AUTH_URL': 'http://no.where'} 38 | 39 | 40 | class BlazarShellTestCase(tests.TestCase): 41 | 42 | def make_env(self, exclude=None, fake_env=FAKE_ENV): 43 | env = dict((k, v) for k, v in fake_env.items() if k != exclude) 44 | self.useFixture(fixtures.MonkeyPatch('os.environ', env)) 45 | 46 | def setUp(self): 47 | super(BlazarShellTestCase, self).setUp() 48 | 49 | #Create shell for non-specific tests 50 | self.blazar_shell = shell.BlazarShell() 51 | 52 | def shell(self, argstr, exitcodes=(0,)): 53 | orig = sys.stdout 54 | orig_stderr = sys.stderr 55 | try: 56 | sys.stdout = io.StringIO() 57 | sys.stderr = io.StringIO() 58 | _shell = shell.BlazarShell() 59 | _shell.initialize_app(argstr.split()) 60 | except SystemExit: 61 | exc_type, exc_value, exc_traceback = sys.exc_info() 62 | self.assertIn(exc_value.code, exitcodes) 63 | finally: 64 | stdout = sys.stdout.getvalue() 65 | sys.stdout.close() 66 | sys.stdout = orig 67 | stderr = sys.stderr.getvalue() 68 | sys.stderr.close() 69 | sys.stderr = orig_stderr 70 | return (stdout, stderr) 71 | 72 | def test_help_unknown_command(self): 73 | self.assertRaises(ValueError, self.shell, 'bash-completion') 74 | 75 | @testtools.skip('lol') 76 | def test_bash_completion(self): 77 | stdout, stderr = self.shell('bash-completion') 78 | # just check we have some output 79 | required = [ 80 | '.*--matching', 81 | '.*--wrap', 82 | '.*help', 83 | '.*secgroup-delete-rule', 84 | '.*--priority'] 85 | for r in required: 86 | self.assertThat((stdout + stderr), 87 | testtools.matchers.MatchesRegex( 88 | r, re.DOTALL | re.MULTILINE)) 89 | 90 | @testtools.skip('lol') 91 | def test_authenticate_user(self): 92 | obj = shell.BlazarShell() 93 | obj.initialize_app('list-leases') 94 | obj.options.os_token = 'aaaa-bbbb-cccc' 95 | obj.options.os_cacert = 'cert' 96 | 97 | obj.authenticate_user() 98 | -------------------------------------------------------------------------------- /blazarclient/v1/hosts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Bull. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from blazarclient import base 17 | from blazarclient import exception 18 | from blazarclient.i18n import _ 19 | 20 | 21 | class ComputeHostClientManager(base.BaseClientManager): 22 | """Manager for the ComputeHost connected requests.""" 23 | 24 | def create(self, name, **kwargs): 25 | """Creates host from values passed.""" 26 | values = {'name': name} 27 | values.update(**kwargs) 28 | resp, body = self.request_manager.post('/os-hosts', body=values) 29 | return body['host'] 30 | 31 | def get(self, host_id): 32 | """Describe host specifications such as name and details.""" 33 | resp, body = self.request_manager.get('/os-hosts/%s' % host_id) 34 | return body['host'] 35 | 36 | def update(self, host_id, values): 37 | """Update attributes of the host.""" 38 | if not values: 39 | return _('No values to update passed.') 40 | resp, body = self.request_manager.put( 41 | '/os-hosts/%s' % host_id, body=values 42 | ) 43 | return body['host'] 44 | 45 | def delete(self, host_id): 46 | """Delete host with specified ID.""" 47 | resp, body = self.request_manager.delete('/os-hosts/%s' % host_id) 48 | 49 | def list(self, sort_by=None): 50 | """List all hosts.""" 51 | resp, body = self.request_manager.get('/os-hosts') 52 | hosts = body['hosts'] 53 | if sort_by: 54 | hosts = sorted(hosts, key=lambda host: host[sort_by]) 55 | return hosts 56 | 57 | def list_properties(self, detail=False, all=False, sort_by=None): 58 | url = '/os-hosts/properties' 59 | 60 | query_parts = [] 61 | if detail: 62 | query_parts.append("detail=True") 63 | if all: 64 | query_parts.append("all=True") 65 | if query_parts: 66 | url += "?" + "&".join(query_parts) 67 | 68 | resp, body = self.request_manager.get(url) 69 | resource_properties = body['resource_properties'] 70 | 71 | # Values is a reserved word in cliff so need to rename values column. 72 | if detail: 73 | for p in resource_properties: 74 | p['property_values'] = p['values'] 75 | del p['values'] 76 | 77 | if sort_by: 78 | resource_properties = sorted(resource_properties, 79 | key=lambda rp: rp[sort_by]) 80 | return resource_properties 81 | 82 | def get_property(self, property_name): 83 | resource_property = [ 84 | x for x in self.list_properties(detail=True) 85 | if x['property'] == property_name] 86 | if not resource_property: 87 | raise exception.ResourcePropertyNotFound() 88 | return resource_property[0] 89 | 90 | def set_property(self, property_name, private): 91 | data = {'private': private} 92 | resp, body = self.request_manager.patch( 93 | '/os-hosts/properties/%s' % property_name, body=data) 94 | 95 | return body['resource_property'] 96 | -------------------------------------------------------------------------------- /blazarclient/v1/leases.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from oslo_utils import timeutils 17 | 18 | from blazarclient import base 19 | from blazarclient.i18n import _ 20 | from blazarclient import utils 21 | 22 | 23 | class LeaseClientManager(base.BaseClientManager): 24 | """Manager for the lease connected requests.""" 25 | 26 | def create(self, name, start, end, reservations, events, before_end=None): 27 | """Creates lease from values passed.""" 28 | values = {'name': name, 'start_date': start, 'end_date': end, 29 | 'reservations': reservations, 'events': events, 30 | 'before_end_date': before_end} 31 | 32 | resp, body = self.request_manager.post('/leases', body=values) 33 | return body['lease'] 34 | 35 | def get(self, lease_id): 36 | """Describes lease specifications such as name, status and locked 37 | condition. 38 | """ 39 | resp, body = self.request_manager.get('/leases/%s' % lease_id) 40 | return body['lease'] 41 | 42 | def update(self, lease_id, name=None, prolong_for=None, reduce_by=None, 43 | end_date=None, advance_by=None, defer_by=None, start_date=None, 44 | reservations=None): 45 | """Update attributes of the lease.""" 46 | values = {} 47 | if name: 48 | values['name'] = name 49 | 50 | lease_end_date_change = prolong_for or reduce_by or end_date 51 | lease_start_date_change = defer_by or advance_by or start_date 52 | lease = None 53 | 54 | if lease_end_date_change: 55 | lease = self.get(lease_id) 56 | if end_date: 57 | date = timeutils.parse_strtime(end_date, utils.API_DATE_FORMAT) 58 | values['end_date'] = date.strftime(utils.API_DATE_FORMAT) 59 | else: 60 | self._add_lease_date(values, lease, 'end_date', 61 | lease_end_date_change, 62 | prolong_for is not None) 63 | 64 | if lease_start_date_change: 65 | if lease is None: 66 | lease = self.get(lease_id) 67 | if start_date: 68 | date = timeutils.parse_strtime(start_date, 69 | utils.API_DATE_FORMAT) 70 | values['start_date'] = date.strftime(utils.API_DATE_FORMAT) 71 | else: 72 | self._add_lease_date(values, lease, 'start_date', 73 | lease_start_date_change, 74 | defer_by is not None) 75 | 76 | if reservations: 77 | values['reservations'] = reservations 78 | 79 | if not values: 80 | return _('No values to update passed.') 81 | resp, body = self.request_manager.put('/leases/%s' % lease_id, 82 | body=values) 83 | return body['lease'] 84 | 85 | def delete(self, lease_id): 86 | """Deletes lease with specified ID.""" 87 | resp, body = self.request_manager.delete('/leases/%s' % lease_id) 88 | 89 | def list(self, sort_by=None): 90 | """List all leases.""" 91 | resp, body = self.request_manager.get('/leases') 92 | leases = body['leases'] 93 | if sort_by: 94 | leases = sorted(leases, key=lambda lease: lease[sort_by]) 95 | return leases 96 | 97 | def _add_lease_date(self, values, lease, key, delta_date, positive_delta): 98 | delta_sec = utils.from_elapsed_time_to_delta( 99 | delta_date, 100 | pos_sign=positive_delta) 101 | date = timeutils.parse_strtime(lease[key], 102 | utils.LEASE_DATE_FORMAT) 103 | values[key] = (date + delta_sec).strftime(utils.API_DATE_FORMAT) 104 | -------------------------------------------------------------------------------- /blazarclient/tests/test_command.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from unittest import mock 17 | 18 | import testtools 19 | 20 | from blazarclient import command 21 | from blazarclient import tests 22 | 23 | 24 | class OpenstackCommandTestCase(tests.TestCase): 25 | 26 | def setUp(self): 27 | super(OpenstackCommandTestCase, self).setUp() 28 | 29 | @testtools.skip("Have no idea how to test super") 30 | def test_run(self): 31 | pass 32 | 33 | @testtools.skip("Unskip it when get_data will do smthg") 34 | def test_get_data(self): 35 | pass 36 | 37 | @testtools.skip("Unskip it when get_data will do smthg") 38 | def test_take_action(self): 39 | pass 40 | 41 | 42 | class TableFormatterTestCase(tests.TestCase): 43 | 44 | def setUp(self): 45 | super(TableFormatterTestCase, self).setUp() 46 | 47 | @testtools.skip("Have no idea how to test super") 48 | def test_emit_list(self): 49 | pass 50 | 51 | 52 | class BlazarCommandTestCase(tests.TestCase): 53 | 54 | def setUp(self): 55 | super(BlazarCommandTestCase, self).setUp() 56 | 57 | self.app = mock.MagicMock() 58 | self.parser = self.patch(command.OpenStackCommand, 'get_parser') 59 | 60 | self.command = command.BlazarCommand(self.app, []) 61 | 62 | def test_get_client(self): 63 | # Test that either client_manager.reservation or client is used, 64 | # whichever exists 65 | 66 | client_manager = self.app.client_manager 67 | del self.app.client_manager 68 | client = self.command.get_client() 69 | self.assertEqual(self.app.client, client) 70 | 71 | self.app.client_manager = client_manager 72 | del self.app.client 73 | client = self.command.get_client() 74 | self.assertEqual(self.app.client_manager.reservation, client) 75 | 76 | def test_get_parser(self): 77 | self.command.get_parser('TestCase') 78 | self.parser.assert_called_once_with('TestCase') 79 | 80 | def test_format_output_data(self): 81 | data_before = {'key_string': 'string_value', 82 | 'key_dict': {'key': 'value'}, 83 | 'key_list': ['1', '2', '3'], 84 | 'key_none': None} 85 | data_after = {'key_string': 'string_value', 86 | 'key_dict': '{"key": "value"}', 87 | 'key_list': '1\n2\n3', 88 | 'key_none': ''} 89 | 90 | self.command.format_output_data(data_before) 91 | 92 | self.assertEqual(data_after, data_before) 93 | 94 | 95 | class CreateCommandTestCase(tests.TestCase): 96 | def setUp(self): 97 | super(CreateCommandTestCase, self).setUp() 98 | 99 | self.app = mock.MagicMock() 100 | self.create_command = command.CreateCommand(self.app, []) 101 | 102 | self.client = self.patch(self.create_command, 'get_client') 103 | 104 | @testtools.skip("Under construction") 105 | def test_get_data_data(self): 106 | data = {'key_string': 'string_value', 107 | 'key_dict': "{'key0': 'value', 'key1': 'value'}", 108 | 'key_list': "['1', '2', '3',]", 109 | 'key_none': None} 110 | self.client.resource.return_value = mock.MagicMock(return_value=data) 111 | self.assertEqual(self.create_command.get_data({'a': 'b'}), None) 112 | 113 | 114 | @testtools.skip("Under construction") 115 | class UpdateCommandTestCase(tests.TestCase): 116 | def setUp(self): 117 | super(UpdateCommandTestCase, self).setUp() 118 | 119 | self.app = mock.MagicMock() 120 | self.update_command = command.UpdateCommand(self.app, []) 121 | 122 | 123 | @testtools.skip("Under construction") 124 | class DeleteCommandTestCase(tests.TestCase): 125 | def setUp(self): 126 | super(DeleteCommandTestCase, self).setUp() 127 | 128 | self.app = mock.MagicMock() 129 | self.delete_command = command.DeleteCommand(self.app, []) 130 | 131 | 132 | @testtools.skip("Under construction") 133 | class ListCommandTestCase(tests.TestCase): 134 | def setUp(self): 135 | super(ListCommandTestCase, self).setUp() 136 | 137 | self.app = mock.MagicMock() 138 | self.list_command = command.ListCommand(self.app, []) 139 | 140 | 141 | @testtools.skip("Under construction") 142 | class ShowCommandTestCase(tests.TestCase): 143 | def setUp(self): 144 | super(ShowCommandTestCase, self).setUp() 145 | 146 | self.app = mock.MagicMock() 147 | self.show_command = command.ShowCommand(self.app, []) 148 | -------------------------------------------------------------------------------- /blazarclient/tests/v1/shell_commands/test_floatingips.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 StackHPC Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import argparse 17 | from unittest import mock 18 | 19 | from blazarclient import shell 20 | from blazarclient import tests 21 | from blazarclient.v1.shell_commands import floatingips 22 | 23 | 24 | class CreateFloatingIPTest(tests.TestCase): 25 | 26 | def setUp(self): 27 | super(CreateFloatingIPTest, self).setUp() 28 | self.create_floatingip = floatingips.CreateFloatingIP( 29 | shell.BlazarShell(), mock.Mock()) 30 | 31 | def test_args2body(self): 32 | args = argparse.Namespace( 33 | network_id='1e17587e-a7ed-4b82-a17b-4beb32523e28', 34 | floating_ip_address='172.24.4.101', 35 | ) 36 | 37 | expected = { 38 | 'network_id': '1e17587e-a7ed-4b82-a17b-4beb32523e28', 39 | 'floating_ip_address': '172.24.4.101', 40 | } 41 | 42 | ret = self.create_floatingip.args2body(args) 43 | self.assertDictEqual(ret, expected) 44 | 45 | 46 | class ListFloatingIPsTest(tests.TestCase): 47 | 48 | def create_list_command(self, list_value): 49 | mock_floatingip_manager = mock.Mock() 50 | mock_floatingip_manager.list.return_value = list_value 51 | 52 | mock_client = mock.Mock() 53 | mock_client.floatingip = mock_floatingip_manager 54 | 55 | blazar_shell = shell.BlazarShell() 56 | blazar_shell.client = mock_client 57 | return (floatingips.ListFloatingIPs(blazar_shell, mock.Mock()), 58 | mock_floatingip_manager) 59 | 60 | def test_list_floatingips(self): 61 | list_value = [ 62 | {'id': '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9'}, 63 | {'id': 'f180cf4c-f886-4dd1-8c36-854d17fbefb5'}, 64 | ] 65 | 66 | list_floatingips, floatingip_manager = self.create_list_command( 67 | list_value) 68 | 69 | args = argparse.Namespace(sort_by='id', columns=['id']) 70 | expected = [['id'], [('84c4d37e-1f8b-45ce-897b-16ad7f49b0e9',), 71 | ('f180cf4c-f886-4dd1-8c36-854d17fbefb5',)]] 72 | 73 | ret = list_floatingips.get_data(args) 74 | self.assertEqual(expected[0], ret[0]) 75 | self.assertEqual(expected[1], [x for x in ret[1]]) 76 | 77 | floatingip_manager.list.assert_called_once_with(sort_by='id') 78 | 79 | 80 | class ShowFloatingIPTest(tests.TestCase): 81 | 82 | def create_show_command(self, list_value, get_value): 83 | mock_floatingip_manager = mock.Mock() 84 | mock_floatingip_manager.list.return_value = list_value 85 | mock_floatingip_manager.get.return_value = get_value 86 | 87 | mock_client = mock.Mock() 88 | mock_client.floatingip = mock_floatingip_manager 89 | 90 | blazar_shell = shell.BlazarShell() 91 | blazar_shell.client = mock_client 92 | return (floatingips.ShowFloatingIP(blazar_shell, mock.Mock()), 93 | mock_floatingip_manager) 94 | 95 | def test_show_floatingip(self): 96 | list_value = [ 97 | {'id': '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9'}, 98 | {'id': 'f180cf4c-f886-4dd1-8c36-854d17fbefb5'}, 99 | ] 100 | get_value = { 101 | 'id': '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9'} 102 | 103 | show_floatingip, floatingip_manager = self.create_show_command( 104 | list_value, get_value) 105 | 106 | args = argparse.Namespace(id='84c4d37e-1f8b-45ce-897b-16ad7f49b0e9') 107 | expected = [('id',), ('84c4d37e-1f8b-45ce-897b-16ad7f49b0e9',)] 108 | 109 | ret = show_floatingip.get_data(args) 110 | self.assertEqual(ret, expected) 111 | 112 | floatingip_manager.get.assert_called_once_with( 113 | '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9') 114 | 115 | 116 | class DeleteFloatingIPTest(tests.TestCase): 117 | 118 | def create_delete_command(self, list_value): 119 | mock_floatingip_manager = mock.Mock() 120 | mock_floatingip_manager.list.return_value = list_value 121 | 122 | mock_client = mock.Mock() 123 | mock_client.floatingip = mock_floatingip_manager 124 | 125 | blazar_shell = shell.BlazarShell() 126 | blazar_shell.client = mock_client 127 | return (floatingips.DeleteFloatingIP(blazar_shell, mock.Mock()), 128 | mock_floatingip_manager) 129 | 130 | def test_delete_floatingip(self): 131 | list_value = [ 132 | {'id': '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9'}, 133 | {'id': 'f180cf4c-f886-4dd1-8c36-854d17fbefb5'}, 134 | ] 135 | delete_floatingip, floatingip_manager = self.create_delete_command( 136 | list_value) 137 | 138 | args = argparse.Namespace(id='84c4d37e-1f8b-45ce-897b-16ad7f49b0e9') 139 | delete_floatingip.run(args) 140 | 141 | floatingip_manager.delete.assert_called_once_with( 142 | '84c4d37e-1f8b-45ce-897b-16ad7f49b0e9') 143 | -------------------------------------------------------------------------------- /blazarclient/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from keystoneauth1 import adapter 17 | from oslo_serialization import jsonutils 18 | import requests 19 | 20 | from blazarclient import exception 21 | from blazarclient.i18n import _ 22 | 23 | 24 | class RequestManager(object): 25 | """Manager to create request from given Blazar URL and auth token.""" 26 | 27 | def __init__(self, blazar_url, auth_token, user_agent): 28 | self.blazar_url = blazar_url 29 | self.auth_token = auth_token 30 | self.user_agent = user_agent 31 | 32 | def get(self, url): 33 | """Sends get request to Blazar. 34 | 35 | :param url: URL to the wanted Blazar resource. 36 | :type url: str 37 | """ 38 | return self.request(url, 'GET') 39 | 40 | def post(self, url, body): 41 | """Sends post request to Blazar. 42 | 43 | :param url: URL to the wanted Blazar resource. 44 | :type url: str 45 | 46 | :param body: Values resource to be created from. 47 | :type body: dict 48 | """ 49 | return self.request(url, 'POST', body=body) 50 | 51 | def delete(self, url): 52 | """Sends delete request to Blazar. 53 | 54 | :param url: URL to the wanted Blazar resource. 55 | :type url: str 56 | """ 57 | return self.request(url, 'DELETE') 58 | 59 | def put(self, url, body): 60 | """Sends update request to Blazar. 61 | 62 | :param url: URL to the wanted Blazar resource. 63 | :type url: str 64 | 65 | :param body: Values resource to be updated from. 66 | :type body: dict 67 | """ 68 | return self.request(url, 'PUT', body=body) 69 | 70 | def patch(self, url, body): 71 | """Sends patch request to Blazar. 72 | 73 | :param url: URL to the wanted Blazar resource. 74 | :type url: str 75 | """ 76 | return self.request(url, 'PATCH', body=body) 77 | 78 | def request(self, url, method, **kwargs): 79 | """Base request method. 80 | 81 | Adds specific headers and URL prefix to the request. 82 | 83 | :param url: Resource URL. 84 | :type url: str 85 | 86 | :param method: Method to be called (GET, POST, PUT, DELETE). 87 | :type method: str 88 | 89 | :returns: Response and body. 90 | :rtype: tuple 91 | """ 92 | kwargs.setdefault('headers', kwargs.get('headers', {})) 93 | kwargs['headers']['User-Agent'] = self.user_agent 94 | kwargs['headers']['Accept'] = 'application/json' 95 | kwargs['headers']['x-auth-token'] = self.auth_token 96 | 97 | if 'body' in kwargs: 98 | kwargs['headers']['Content-Type'] = 'application/json' 99 | kwargs['data'] = jsonutils.dump_as_bytes(kwargs['body']) 100 | del kwargs['body'] 101 | 102 | resp = requests.request(method, self.blazar_url + url, **kwargs) 103 | 104 | try: 105 | body = jsonutils.loads(resp.text) 106 | except ValueError: 107 | body = None 108 | 109 | if resp.status_code >= 400: 110 | if body is not None: 111 | error_message = body.get('error_message', body) 112 | else: 113 | error_message = resp.text 114 | 115 | body = _("ERROR: {0}").format(error_message) 116 | raise exception.BlazarClientException(body, code=resp.status_code) 117 | 118 | return resp, body 119 | 120 | 121 | class SessionClient(adapter.LegacyJsonAdapter): 122 | """Manager to create request with keystoneauth1 session.""" 123 | 124 | def request(self, url, method, **kwargs): 125 | resp, body = super(SessionClient, self).request( 126 | url, method, raise_exc=False, **kwargs) 127 | 128 | if resp.status_code >= 400: 129 | if body is not None: 130 | error_message = body.get('error_message', body) 131 | else: 132 | error_message = resp.text 133 | 134 | msg = _("ERROR: {0}").format(error_message) 135 | raise exception.BlazarClientException(msg, code=resp.status_code) 136 | 137 | return resp, body 138 | 139 | 140 | class BaseClientManager(object): 141 | """Base class for managing resources of Blazar.""" 142 | 143 | user_agent = 'python-blazarclient' 144 | 145 | def __init__(self, blazar_url, auth_token, session, **kwargs): 146 | self.blazar_url = blazar_url 147 | self.auth_token = auth_token 148 | self.session = session 149 | 150 | if self.session: 151 | self.request_manager = SessionClient( 152 | session=self.session, 153 | user_agent=self.user_agent, 154 | **kwargs 155 | ) 156 | elif self.blazar_url and self.auth_token: 157 | self.request_manager = RequestManager(blazar_url=self.blazar_url, 158 | auth_token=self.auth_token, 159 | user_agent=self.user_agent) 160 | else: 161 | raise exception.InsufficientAuthInformation 162 | -------------------------------------------------------------------------------- /blazarclient/v1/shell_commands/allocations.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 University of Chicago. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | 18 | from blazarclient import command 19 | from blazarclient import utils 20 | 21 | RESOURCE_ID_PATTERN = '^[0-9]+$' 22 | 23 | 24 | class ShowAllocations(command.ShowCommand): 25 | """Show allocations for resource identified by type and ID.""" 26 | resource = 'allocation' 27 | json_indent = 4 28 | id_pattern = RESOURCE_ID_PATTERN 29 | name_key = 'hypervisor_hostname' 30 | log = logging.getLogger(__name__ + '.ShowHostAllocation') 31 | 32 | def get_parser(self, prog_name): 33 | parser = super(ShowAllocations, self).get_parser(prog_name) 34 | parser.add_argument( 35 | 'resource_type', 36 | choices=['host'], 37 | help='Show allocations for a resource type' 38 | ) 39 | if self.allow_names: 40 | help_str = 'ID or name of %s to look up' 41 | else: 42 | help_str = 'ID of %s to look up' 43 | parser.add_argument('id', metavar="RESOURCE", 44 | help=help_str % "resource") 45 | parser.add_argument( 46 | '--reservation-id', 47 | dest='reservation_id', 48 | default=None, 49 | help='Show only allocations with specific reservation_id' 50 | ) 51 | parser.add_argument( 52 | '--lease-id', 53 | dest='lease_id', 54 | default=None, 55 | help='Show only allocations with specific lease_id' 56 | ) 57 | return parser 58 | 59 | def get_data(self, parsed_args): 60 | self.log.debug('get_data(%s)' % parsed_args) 61 | blazar_client = self.get_client() 62 | resource_manager = getattr(blazar_client, self.resource) 63 | 64 | if self.allow_names: 65 | res_id = utils.find_resource_id_by_name_or_id( 66 | blazar_client, 67 | parsed_args.resource_type, 68 | parsed_args.id, 69 | self.name_key, 70 | self.id_pattern) 71 | else: 72 | res_id = parsed_args.id 73 | 74 | data = resource_manager.get( 75 | self.args2body(parsed_args)['resource'], res_id) 76 | 77 | if parsed_args.lease_id is not None: 78 | data['reservations'] = list( 79 | filter(lambda d: d['lease_id'] == parsed_args.lease_id, 80 | data['reservations'])) 81 | if parsed_args.reservation_id is not None: 82 | data['reservations'] = list( 83 | filter(lambda d: d['id'] == parsed_args.reservation_id, 84 | data['reservations'])) 85 | 86 | self.format_output_data(data) 87 | return list(zip(*sorted(data.items()))) 88 | 89 | def args2body(self, parsed_args): 90 | params = {} 91 | if parsed_args.resource_type == 'host': 92 | params.update(dict(resource='os-hosts')) 93 | return params 94 | 95 | 96 | class ListAllocations(command.ListCommand): 97 | """List allocations for all resources of a type.""" 98 | resource = 'allocation' 99 | log = logging.getLogger(__name__ + '.ListHostAllocations') 100 | list_columns = ['resource_id', 'reservations'] 101 | 102 | def get_parser(self, prog_name): 103 | parser = super(ListAllocations, self).get_parser(prog_name) 104 | 105 | parser.add_argument( 106 | 'resource_type', 107 | choices=['host'], 108 | help='Show allocations for a resource type' 109 | ) 110 | parser.add_argument( 111 | '--reservation-id', 112 | dest='reservation_id', 113 | default=None, 114 | help='Show only allocations with specific reservation_id' 115 | ) 116 | parser.add_argument( 117 | '--lease-id', 118 | dest='lease_id', 119 | default=None, 120 | help='Show only allocations with specific lease_id' 121 | ) 122 | parser.add_argument( 123 | '--sort-by', metavar="", 124 | help='column name used to sort result', 125 | default='resource_id' 126 | ) 127 | return parser 128 | 129 | def get_data(self, parsed_args): 130 | self.log.debug('get_data(%s)' % parsed_args) 131 | data = self.retrieve_list(parsed_args) 132 | 133 | for resource in data: 134 | if parsed_args.lease_id is not None: 135 | resource['reservations'] = list( 136 | filter(lambda d: d['lease_id'] == parsed_args.lease_id, 137 | resource['reservations'])) 138 | if parsed_args.reservation_id is not None: 139 | resource['reservations'] = list( 140 | filter(lambda d: d['id'] == parsed_args.reservation_id, 141 | resource['reservations'])) 142 | 143 | return self.setup_columns(data, parsed_args) 144 | 145 | def args2body(self, parsed_args): 146 | params = super(ListAllocations, self).args2body(parsed_args) 147 | if parsed_args.resource_type == 'host': 148 | params.update(dict(resource='os-hosts')) 149 | return params 150 | -------------------------------------------------------------------------------- /releasenotes/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 11 | # implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Configuration file for the Sphinx documentation builder. 16 | # 17 | # This file does only contain a selection of the most common options. For a 18 | # full list see the documentation: 19 | # http://www.sphinx-doc.org/en/master/config 20 | 21 | # -- Path setup -------------------------------------------------------------- 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | # 27 | # import os 28 | # import sys 29 | # sys.path.insert(0, os.path.abspath('.')) 30 | 31 | 32 | # -- Project information ----------------------------------------------------- 33 | 34 | project = 'Blazar Client Release Notes' 35 | copyright = '2018, Blazar Developers' 36 | author = 'Blazar Developers' 37 | 38 | # The short X.Y version 39 | version = '' 40 | # The full version, including alpha/beta/rc tags 41 | release = '' 42 | 43 | 44 | # -- General configuration --------------------------------------------------- 45 | 46 | # If your documentation needs a minimal Sphinx version, state it here. 47 | # 48 | # needs_sphinx = '1.0' 49 | 50 | # Add any Sphinx extension module names here, as strings. They can be 51 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 52 | # ones. 53 | extensions = [ 54 | 'openstackdocstheme', 55 | 'reno.sphinxext', 56 | ] 57 | 58 | # openstackdocstheme options 59 | openstackdocs_repo_name = 'openstack/python-blazarclient' 60 | openstackdocs_bug_project = 'blazar' 61 | openstackdocs_bug_tag = '' 62 | 63 | # Add any paths that contain templates here, relative to this directory. 64 | templates_path = ['_templates'] 65 | 66 | # The suffix(es) of source filenames. 67 | # You can specify multiple suffix as a list of string: 68 | # 69 | # source_suffix = ['.rst', '.md'] 70 | source_suffix = '.rst' 71 | 72 | # The master toctree document. 73 | master_doc = 'index' 74 | 75 | # The language for content autogenerated by Sphinx. Refer to documentation 76 | # for a list of supported languages. 77 | # 78 | # This is also used if you do content translation via gettext catalogs. 79 | # Usually you set "language" from the command line for these cases. 80 | # language = None 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | # This pattern also affects html_static_path and html_extra_path . 85 | exclude_patterns = [] 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'native' 89 | 90 | 91 | # -- Options for HTML output ------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | # 96 | html_theme = 'openstackdocs' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | # 102 | # html_theme_options = {} 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ['_static'] 108 | 109 | # Custom sidebar templates, must be a dictionary that maps document names 110 | # to template names. 111 | # 112 | # The default sidebars (for documents that don't match any pattern) are 113 | # defined by theme itself. Builtin themes are using these templates by 114 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 115 | # 'searchbox.html']``. 116 | # 117 | # html_sidebars = {} 118 | 119 | 120 | # -- Options for HTMLHelp output --------------------------------------------- 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'BlazarClientReleaseNotesdoc' 124 | 125 | 126 | # -- Options for LaTeX output ------------------------------------------------ 127 | 128 | latex_elements = { 129 | # The paper size ('letterpaper' or 'a4paper'). 130 | # 131 | # 'papersize': 'letterpaper', 132 | 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 139 | # 'preamble': '', 140 | 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, 'BlazarClientReleaseNotes.tex', 151 | 'Blazar Client Release Notes', 'Blazar Developers', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output ------------------------------------------ 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'blazarclientreleasenotes', 'Blazar Client Release Notes', 161 | [author], 1) 162 | ] 163 | 164 | 165 | # -- Options for Texinfo output ---------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | (master_doc, 'BlazarClientReleaseNotes', 'Blazar Client Release Notes', 172 | author, 'BlazarClientReleaseNotes', 'Reservation service client.', 173 | 'Miscellaneous'), 174 | ] 175 | 176 | # -- Options for Internationalization output ------------------------------ 177 | locale_dirs = ['locale/'] 178 | -------------------------------------------------------------------------------- /blazarclient/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import datetime 17 | import os 18 | import re 19 | 20 | from oslo_serialization import jsonutils as json 21 | 22 | from blazarclient import exception 23 | from blazarclient.i18n import _ 24 | 25 | ELAPSED_TIME_REGEX = r'^(\d+)([s|m|h|d])$' 26 | 27 | LEASE_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' 28 | API_DATE_FORMAT = '%Y-%m-%d %H:%M' 29 | 30 | 31 | def env(*args, **kwargs): 32 | """Returns the first environment variable set. 33 | 34 | if none are non-empty, defaults to '' or keyword arg default. 35 | """ 36 | for v in args: 37 | value = os.environ.get(v) 38 | if value: 39 | return value 40 | return kwargs.get('default', '') 41 | 42 | 43 | def to_primitive(value): 44 | if isinstance(value, list) or isinstance(value, tuple): 45 | o = [] 46 | for v in value: 47 | o.append(to_primitive(v)) 48 | return o 49 | elif isinstance(value, dict): 50 | o = {} 51 | for k, v in value.items(): 52 | o[k] = to_primitive(v) 53 | return o 54 | elif isinstance(value, datetime.datetime): 55 | return str(value) 56 | elif hasattr(value, '__iter__'): 57 | return to_primitive(list(value)) 58 | else: 59 | return value 60 | 61 | 62 | def dumps(value, indent=None): 63 | try: 64 | return json.dumps(value, indent=indent) 65 | except TypeError: 66 | pass 67 | return json.dumps(to_primitive(value)) 68 | 69 | 70 | def get_item_properties(item, fields, mixed_case_fields=None, formatters=None): 71 | """Return a tuple containing the item properties. 72 | 73 | :param item: a single item resource (e.g. Server, Tenant, etc) 74 | :param fields: tuple of strings with the desired field names 75 | :param mixed_case_fields: tuple of field names to preserve case 76 | :param formatters: dictionary mapping field names to callables 77 | to format the values 78 | """ 79 | row = [] 80 | if mixed_case_fields is None: 81 | mixed_case_fields = [] 82 | if formatters is None: 83 | formatters = {} 84 | 85 | for field in fields: 86 | if field in formatters: 87 | row.append(formatters[field](item)) 88 | else: 89 | if field in mixed_case_fields: 90 | field_name = field.replace(' ', '_') 91 | else: 92 | field_name = field.lower().replace(' ', '_') 93 | if not hasattr(item, field_name) and isinstance(item, dict): 94 | data = item[field_name] 95 | else: 96 | data = getattr(item, field_name, '') 97 | if data is None: 98 | data = '' 99 | row.append(data) 100 | return tuple(row) 101 | 102 | 103 | def find_resource_id_by_name_or_id(client, resource_type, name_or_id, 104 | name_key, id_pattern): 105 | if re.match(id_pattern, name_or_id): 106 | return name_or_id 107 | 108 | return _find_resource_id_by_name(client, resource_type, name_or_id, 109 | name_key) 110 | 111 | 112 | def _find_resource_id_by_name(client, resource_type, name, name_key): 113 | resource_manager = getattr(client, resource_type) 114 | resources = resource_manager.list() 115 | 116 | named_resources = [] 117 | key = name_key if name_key else 'name' 118 | 119 | for resource in resources: 120 | if resource[key] == name: 121 | named_resources.append(resource['id']) 122 | if len(named_resources) > 1: 123 | raise exception.NoUniqueMatch(message="There are more than one " 124 | "appropriate resources for the " 125 | "name '%s' and type '%s'" % 126 | (name, resource_type)) 127 | elif named_resources: 128 | return named_resources[0] 129 | else: 130 | message = "Unable to find resource with name '%s'" % name 131 | raise exception.BlazarClientException(message=message, 132 | status_code=404) 133 | 134 | 135 | def from_elapsed_time_to_seconds(elapsed_time, pos_sign=True): 136 | """Return the positive or negative amount of seconds based on the 137 | elapsed_time parameter with a sign depending on the sign parameter. 138 | :param: elapsed_time: a string that matches ELAPSED_TIME_REGEX 139 | :param: sign: if pos_sign is True, the returned value will be positive. 140 | Otherwise it will be positive. 141 | """ 142 | is_elapsed_time = re.match(ELAPSED_TIME_REGEX, elapsed_time) 143 | if is_elapsed_time is None: 144 | raise exception.BlazarClientException(_("Invalid time " 145 | "format for option.")) 146 | elapsed_time_value = int(is_elapsed_time.group(1)) 147 | elapsed_time_option = is_elapsed_time.group(2) 148 | seconds = { 149 | 's': lambda x: 150 | datetime.timedelta(seconds=x).total_seconds(), 151 | 'm': lambda x: 152 | datetime.timedelta(minutes=x).total_seconds(), 153 | 'h': lambda x: 154 | datetime.timedelta(hours=x).total_seconds(), 155 | 'd': lambda x: 156 | datetime.timedelta(days=x).total_seconds(), 157 | }[elapsed_time_option](elapsed_time_value) 158 | 159 | # the above code returns a "float" 160 | if pos_sign: 161 | return int(seconds) 162 | return int(seconds) * -1 163 | 164 | 165 | def from_elapsed_time_to_delta(elapsed_time, pos_sign=True): 166 | """Return the positive or negative delta time based on the 167 | elapsed_time parameter. 168 | :param: elapsed_time: a string that matches ELAPSED_TIME_REGEX 169 | :param: sign: if sign is True, the returned value will be negative. 170 | Otherwise it will be positive. 171 | """ 172 | seconds = from_elapsed_time_to_seconds(elapsed_time, pos_sign=pos_sign) 173 | return datetime.timedelta(seconds=seconds) 174 | -------------------------------------------------------------------------------- /blazarclient/v1/shell_commands/hosts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Bull. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import logging 17 | 18 | from blazarclient import command 19 | from blazarclient import exception 20 | 21 | HOST_ID_PATTERN = '^[0-9]+$' 22 | 23 | 24 | class ListHosts(command.ListCommand): 25 | """Print a list of hosts.""" 26 | resource = 'host' 27 | log = logging.getLogger(__name__ + '.ListHosts') 28 | list_columns = ['id', 'hypervisor_hostname', 'vcpus', 'memory_mb', 29 | 'local_gb'] 30 | 31 | def get_parser(self, prog_name): 32 | parser = super(ListHosts, self).get_parser(prog_name) 33 | parser.add_argument( 34 | '--sort-by', metavar="", 35 | help='column name used to sort result', 36 | default='hypervisor_hostname' 37 | ) 38 | return parser 39 | 40 | 41 | class ShowHost(command.ShowCommand): 42 | """Show host details.""" 43 | resource = 'host' 44 | json_indent = 4 45 | name_key = 'hypervisor_hostname' 46 | id_pattern = HOST_ID_PATTERN 47 | log = logging.getLogger(__name__ + '.ShowHost') 48 | 49 | def get_parser(self, prog_name): 50 | parser = super(ShowHost, self).get_parser(prog_name) 51 | if self.allow_names: 52 | help_str = 'ID or name of %s to look up' 53 | else: 54 | help_str = 'ID of %s to look up' 55 | parser.add_argument('id', metavar=self.resource.upper(), 56 | help=help_str % self.resource) 57 | return parser 58 | 59 | 60 | class CreateHost(command.CreateCommand): 61 | """Create a host.""" 62 | resource = 'host' 63 | json_indent = 4 64 | log = logging.getLogger(__name__ + '.CreateHost') 65 | 66 | def get_parser(self, prog_name): 67 | parser = super(CreateHost, self).get_parser(prog_name) 68 | parser.add_argument( 69 | 'name', metavar=self.resource.upper(), 70 | help='Name of the host to add' 71 | ) 72 | parser.add_argument( 73 | '--extra', metavar='=', 74 | action='append', 75 | dest='extra_capabilities', 76 | default=[], 77 | help='Extra capabilities key/value pairs to add for the host' 78 | ) 79 | return parser 80 | 81 | def args2body(self, parsed_args): 82 | params = {} 83 | if parsed_args.name: 84 | params['name'] = parsed_args.name 85 | extras = {} 86 | if parsed_args.extra_capabilities: 87 | for capa in parsed_args.extra_capabilities: 88 | key, _sep, value = capa.partition('=') 89 | # NOTE(sbauza): multiple copies of the same capability will 90 | # result in only the last value to be stored 91 | extras[key] = value 92 | params.update(extras) 93 | return params 94 | 95 | 96 | class UpdateHost(command.UpdateCommand): 97 | """Update attributes of a host.""" 98 | resource = 'host' 99 | json_indent = 4 100 | log = logging.getLogger(__name__ + '.UpdateHost') 101 | name_key = 'hypervisor_hostname' 102 | id_pattern = HOST_ID_PATTERN 103 | 104 | def get_parser(self, prog_name): 105 | parser = super(UpdateHost, self).get_parser(prog_name) 106 | parser.add_argument( 107 | '--extra', metavar='=', 108 | action='append', 109 | dest='extra_capabilities', 110 | default=[], 111 | help='Extra capabilities key/value pairs to update for the host' 112 | ) 113 | return parser 114 | 115 | def args2body(self, parsed_args): 116 | params = {} 117 | extras = {} 118 | if parsed_args.extra_capabilities: 119 | for capa in parsed_args.extra_capabilities: 120 | key, _sep, value = capa.partition('=') 121 | # NOTE(sbauza): multiple copies of the same capability will 122 | # result in only the last value to be stored 123 | extras[key] = value 124 | params['values'] = extras 125 | return params 126 | 127 | 128 | class DeleteHost(command.DeleteCommand): 129 | """Delete a host.""" 130 | resource = 'host' 131 | log = logging.getLogger(__name__ + '.DeleteHost') 132 | name_key = 'hypervisor_hostname' 133 | id_pattern = HOST_ID_PATTERN 134 | 135 | 136 | class ShowHostProperty(command.ShowPropertyCommand): 137 | """Show host property.""" 138 | resource = 'host' 139 | json_indent = 4 140 | log = logging.getLogger(__name__ + '.ShowHostProperty') 141 | 142 | 143 | class ListHostProperties(command.ListCommand): 144 | """List host properties.""" 145 | resource = 'host' 146 | log = logging.getLogger(__name__ + '.ListHostProperties') 147 | list_columns = ['property', 'private', 'property_values'] 148 | 149 | def args2body(self, parsed_args): 150 | params = { 151 | 'detail': parsed_args.detail, 152 | 'all': parsed_args.all, 153 | } 154 | if parsed_args.sort_by: 155 | if parsed_args.sort_by in self.list_columns: 156 | params['sort_by'] = parsed_args.sort_by 157 | else: 158 | msg = 'Invalid sort option %s' % parsed_args.sort_by 159 | raise exception.BlazarClientException(msg) 160 | 161 | return params 162 | 163 | def retrieve_list(self, parsed_args): 164 | """Retrieve a list of resources from Blazar server.""" 165 | blazar_client = self.get_client() 166 | body = self.args2body(parsed_args) 167 | resource_manager = getattr(blazar_client, self.resource) 168 | data = resource_manager.list_properties(**body) 169 | return data 170 | 171 | def get_parser(self, prog_name): 172 | parser = super(ListHostProperties, self).get_parser(prog_name) 173 | parser.add_argument( 174 | '--detail', 175 | action='store_true', 176 | help='Return properties with values and attributes.', 177 | default=False 178 | ) 179 | parser.add_argument( 180 | '--sort-by', metavar="", 181 | help='column name used to sort result', 182 | default='property' 183 | ) 184 | parser.add_argument( 185 | '--all', 186 | action='store_true', 187 | help='Return all properties, public and private.', 188 | default=False 189 | ) 190 | return parser 191 | 192 | 193 | class UpdateHostProperty(command.UpdatePropertyCommand): 194 | """Update attributes of a host property.""" 195 | resource = 'host' 196 | json_indent = 4 197 | log = logging.getLogger(__name__ + '.UpdateHostProperty') 198 | name_key = 'property_name' 199 | -------------------------------------------------------------------------------- /blazarclient/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | from unittest import mock 18 | 19 | from blazarclient import base 20 | from blazarclient import exception 21 | from blazarclient import tests 22 | 23 | 24 | class RequestManagerTestCase(tests.TestCase): 25 | 26 | def setUp(self): 27 | super(RequestManagerTestCase, self).setUp() 28 | 29 | self.blazar_url = "www.fake.com/reservation" 30 | self.auth_token = "aaa-bbb-ccc" 31 | self.user_agent = "python-blazarclient" 32 | self.manager = base.RequestManager(blazar_url=self.blazar_url, 33 | auth_token=self.auth_token, 34 | user_agent=self.user_agent) 35 | 36 | @mock.patch('blazarclient.base.RequestManager.request', 37 | return_value=(200, {"fake": "FAKE"})) 38 | def test_get(self, m): 39 | url = '/leases' 40 | resp, body = self.manager.get(url) 41 | self.assertEqual(resp, 200) 42 | self.assertDictEqual(body, {"fake": "FAKE"}) 43 | m.assert_called_once_with(url, "GET") 44 | 45 | @mock.patch('blazarclient.base.RequestManager.request', 46 | return_value=(200, {"fake": "FAKE"})) 47 | def test_post(self, m): 48 | url = '/leases' 49 | req_body = { 50 | 'start': '2020-07-24 20:00', 51 | 'end': '2020-08-09 22:30', 52 | 'before_end': '2020-08-09 21:30', 53 | 'events': [], 54 | 'name': 'lease-test', 55 | 'reservations': [ 56 | { 57 | 'min': '1', 58 | 'max': '2', 59 | 'hypervisor_properties': 60 | '[">=", "$vcpus", "2"]', 61 | 'resource_properties': 62 | '["==", "$extra_key", "extra_value"]', 63 | 'resource_type': 'physical:host', 64 | 'before_end': 'default' 65 | } 66 | ] 67 | } 68 | resp, body = self.manager.post(url, req_body) 69 | self.assertEqual(resp, 200) 70 | self.assertDictEqual(body, {"fake": "FAKE"}) 71 | m.assert_called_once_with(url, "POST", body=req_body) 72 | 73 | @mock.patch('blazarclient.base.RequestManager.request', 74 | return_value=(200, {"fake": "FAKE"})) 75 | def test_delete(self, m): 76 | url = '/leases/aaa-bbb-ccc' 77 | resp, body = self.manager.delete(url) 78 | self.assertEqual(resp, 200) 79 | self.assertDictEqual(body, {"fake": "FAKE"}) 80 | m.assert_called_once_with(url, "DELETE") 81 | 82 | @mock.patch('blazarclient.base.RequestManager.request', 83 | return_value=(200, {"fake": "FAKE"})) 84 | def test_put(self, m): 85 | url = '/leases/aaa-bbb-ccc' 86 | req_body = { 87 | 'name': 'lease-test', 88 | } 89 | resp, body = self.manager.put(url, req_body) 90 | self.assertEqual(resp, 200) 91 | self.assertDictEqual(body, {"fake": "FAKE"}) 92 | m.assert_called_once_with(url, "PUT", body=req_body) 93 | 94 | @mock.patch('requests.request') 95 | def test_request_ok_with_body(self, m): 96 | m.return_value.status_code = 200 97 | m.return_value.text = '{"resp_key": "resp_value"}' 98 | url = '/leases' 99 | kwargs = {"body": {"req_key": "req_value"}} 100 | self.assertEqual(self.manager.request(url, "POST", **kwargs), 101 | (m(), {"resp_key": "resp_value"})) 102 | 103 | @mock.patch('requests.request') 104 | def test_request_ok_without_body(self, m): 105 | m.return_value.status_code = 200 106 | m.return_value.text = "resp" 107 | url = '/leases' 108 | kwargs = {"body": {"req_key": "req_value"}} 109 | self.assertEqual(self.manager.request(url, "POST", **kwargs), 110 | (m(), None)) 111 | 112 | @mock.patch('requests.request') 113 | def test_request_fail_with_body(self, m): 114 | m.return_value.status_code = 400 115 | m.return_value.text = '{"resp_key": "resp_value"}' 116 | url = '/leases' 117 | kwargs = {"body": {"req_key": "req_value"}} 118 | self.assertRaises(exception.BlazarClientException, 119 | self.manager.request, url, "POST", **kwargs) 120 | 121 | @mock.patch('requests.request') 122 | def test_request_fail_without_body(self, m): 123 | m.return_value.status_code = 400 124 | m.return_value.text = "resp" 125 | url = '/leases' 126 | kwargs = {"body": {"req_key": "req_value"}} 127 | self.assertRaises(exception.BlazarClientException, 128 | self.manager.request, url, "POST", **kwargs) 129 | 130 | 131 | class SessionClientTestCase(tests.TestCase): 132 | 133 | def setUp(self): 134 | super(SessionClientTestCase, self).setUp() 135 | self.manager = base.SessionClient(user_agent="python-blazarclient", 136 | session=mock.MagicMock()) 137 | 138 | @mock.patch('blazarclient.base.adapter.LegacyJsonAdapter.request') 139 | def test_request_ok(self, m): 140 | mock_resp = mock.Mock() 141 | mock_resp.status_code = 200 142 | mock_body = {"resp_key": "resp_value"} 143 | m.return_value = (mock_resp, mock_body) 144 | url = '/leases' 145 | kwargs = {"body": {"req_key": "req_value"}} 146 | resp, body = self.manager.request(url, "POST", **kwargs) 147 | self.assertEqual((resp, body), (mock_resp, mock_body)) 148 | 149 | @mock.patch('blazarclient.base.adapter.LegacyJsonAdapter.request') 150 | def test_request_fail(self, m): 151 | resp = mock.Mock() 152 | resp.status_code = 400 153 | body = {"error message": "error"} 154 | m.return_value = (resp, body) 155 | url = '/leases' 156 | kwargs = {"body": {"req_key": "req_value"}} 157 | self.assertRaises(exception.BlazarClientException, 158 | self.manager.request, url, "POST", **kwargs) 159 | 160 | 161 | class BaseClientManagerTestCase(tests.TestCase): 162 | 163 | def setUp(self): 164 | super(BaseClientManagerTestCase, self).setUp() 165 | 166 | self.blazar_url = "www.fake.com/reservation" 167 | self.auth_token = "aaa-bbb-ccc" 168 | self.session = mock.MagicMock() 169 | self.user_agent = "python-blazarclient" 170 | 171 | def test_init_with_session(self): 172 | manager = base.BaseClientManager(blazar_url=None, 173 | auth_token=None, 174 | session=self.session) 175 | self.assertIsInstance(manager.request_manager, 176 | base.SessionClient) 177 | 178 | def test_init_with_url_and_token(self): 179 | manager = base.BaseClientManager(blazar_url=self.blazar_url, 180 | auth_token=self.auth_token, 181 | session=None) 182 | self.assertIsInstance(manager.request_manager, 183 | base.RequestManager) 184 | 185 | def test_init_with_insufficient_info(self): 186 | self.assertRaises(exception.InsufficientAuthInformation, 187 | base.BaseClientManager, 188 | blazar_url=None, 189 | auth_token=self.auth_token, 190 | session=None) 191 | -------------------------------------------------------------------------------- /blazarclient/tests/v1/shell_commands/test_hosts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 NTT 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import argparse 17 | from unittest import mock 18 | 19 | from blazarclient import shell 20 | from blazarclient import tests 21 | from blazarclient.v1.shell_commands import hosts 22 | 23 | 24 | class CreateHostTest(tests.TestCase): 25 | 26 | def setUp(self): 27 | super(CreateHostTest, self).setUp() 28 | self.create_host = hosts.CreateHost(shell.BlazarShell(), mock.Mock()) 29 | 30 | def test_args2body(self): 31 | args = argparse.Namespace( 32 | name='test-host', 33 | extra_capabilities=[ 34 | 'extra_key1=extra_value1', 35 | 'extra_key2=extra_value2', 36 | ] 37 | ) 38 | 39 | expected = { 40 | 'name': 'test-host', 41 | 'extra_key1': 'extra_value1', 42 | 'extra_key2': 'extra_value2', 43 | } 44 | 45 | ret = self.create_host.args2body(args) 46 | self.assertDictEqual(ret, expected) 47 | 48 | 49 | class UpdateHostTest(tests.TestCase): 50 | 51 | def create_update_command(self, list_value): 52 | mock_host_manager = mock.Mock() 53 | mock_host_manager.list.return_value = list_value 54 | 55 | mock_client = mock.Mock() 56 | mock_client.host = mock_host_manager 57 | 58 | blazar_shell = shell.BlazarShell() 59 | blazar_shell.client = mock_client 60 | return hosts.UpdateHost(blazar_shell, mock.Mock()), mock_host_manager 61 | 62 | def test_update_host(self): 63 | list_value = [ 64 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 65 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 66 | ] 67 | update_host, host_manager = self.create_update_command(list_value) 68 | args = argparse.Namespace( 69 | id='101', 70 | extra_capabilities=[ 71 | 'key1=value1', 72 | 'key2=value2' 73 | ]) 74 | expected = { 75 | 'values': { 76 | 'key1': 'value1', 77 | 'key2': 'value2' 78 | } 79 | } 80 | update_host.run(args) 81 | host_manager.update.assert_called_once_with('101', **expected) 82 | 83 | def test_update_host_with_name(self): 84 | list_value = [ 85 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 86 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 87 | ] 88 | update_host, host_manager = self.create_update_command(list_value) 89 | args = argparse.Namespace( 90 | id='host-1', 91 | extra_capabilities=[ 92 | 'key1=value1', 93 | 'key2=value2' 94 | ]) 95 | expected = { 96 | 'values': { 97 | 'key1': 'value1', 98 | 'key2': 'value2' 99 | } 100 | } 101 | update_host.run(args) 102 | host_manager.update.assert_called_once_with('101', **expected) 103 | 104 | def test_update_host_with_name_startwith_number(self): 105 | list_value = [ 106 | {'id': '101', 'hypervisor_hostname': '1-host'}, 107 | {'id': '201', 'hypervisor_hostname': '2-host'}, 108 | ] 109 | update_host, host_manager = self.create_update_command(list_value) 110 | args = argparse.Namespace( 111 | id='1-host', 112 | extra_capabilities=[ 113 | 'key1=value1', 114 | 'key2=value2' 115 | ]) 116 | expected = { 117 | 'values': { 118 | 'key1': 'value1', 119 | 'key2': 'value2' 120 | } 121 | } 122 | update_host.run(args) 123 | host_manager.update.assert_called_once_with('101', **expected) 124 | 125 | 126 | class ShowHostTest(tests.TestCase): 127 | 128 | def create_show_command(self, list_value, get_value): 129 | mock_host_manager = mock.Mock() 130 | mock_host_manager.list.return_value = list_value 131 | mock_host_manager.get.return_value = get_value 132 | 133 | mock_client = mock.Mock() 134 | mock_client.host = mock_host_manager 135 | 136 | blazar_shell = shell.BlazarShell() 137 | blazar_shell.client = mock_client 138 | return hosts.ShowHost(blazar_shell, mock.Mock()), mock_host_manager 139 | 140 | def test_show_host(self): 141 | list_value = [ 142 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 143 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 144 | ] 145 | get_value = { 146 | 'id': '101', 'hypervisor_hostname': 'host-1'} 147 | 148 | show_host, host_manager = self.create_show_command(list_value, 149 | get_value) 150 | 151 | args = argparse.Namespace(id='101') 152 | expected = [('hypervisor_hostname', 'id'), ('host-1', '101')] 153 | 154 | ret = show_host.get_data(args) 155 | self.assertEqual(ret, expected) 156 | 157 | host_manager.get.assert_called_once_with('101') 158 | 159 | def test_show_host_with_name(self): 160 | list_value = [ 161 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 162 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 163 | ] 164 | get_value = { 165 | 'id': '101', 'hypervisor_hostname': 'host-1'} 166 | 167 | show_host, host_manager = self.create_show_command(list_value, 168 | get_value) 169 | 170 | args = argparse.Namespace(id='host-1') 171 | expected = [('hypervisor_hostname', 'id'), ('host-1', '101')] 172 | 173 | ret = show_host.get_data(args) 174 | self.assertEqual(ret, expected) 175 | 176 | host_manager.get.assert_called_once_with('101') 177 | 178 | def test_show_host_with_name_startwith_number(self): 179 | list_value = [ 180 | {'id': '101', 'hypervisor_hostname': '1-host'}, 181 | {'id': '201', 'hypervisor_hostname': '2-host'}, 182 | ] 183 | get_value = { 184 | 'id': '101', 'hypervisor_hostname': '1-host'} 185 | 186 | show_host, host_manager = self.create_show_command(list_value, 187 | get_value) 188 | args = argparse.Namespace(id='1-host') 189 | expected = [('hypervisor_hostname', 'id'), ('1-host', '101')] 190 | 191 | ret = show_host.get_data(args) 192 | self.assertEqual(ret, expected) 193 | 194 | host_manager.get.assert_called_once_with('101') 195 | 196 | 197 | class DeleteHostTest(tests.TestCase): 198 | 199 | def create_delete_command(self, list_value): 200 | mock_host_manager = mock.Mock() 201 | mock_host_manager.list.return_value = list_value 202 | 203 | mock_client = mock.Mock() 204 | mock_client.host = mock_host_manager 205 | 206 | blazar_shell = shell.BlazarShell() 207 | blazar_shell.client = mock_client 208 | return hosts.DeleteHost(blazar_shell, mock.Mock()), mock_host_manager 209 | 210 | def test_delete_host(self): 211 | list_value = [ 212 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 213 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 214 | ] 215 | delete_host, host_manager = self.create_delete_command(list_value) 216 | 217 | args = argparse.Namespace(id='101') 218 | delete_host.run(args) 219 | 220 | host_manager.delete.assert_called_once_with('101') 221 | 222 | def test_delete_host_with_name(self): 223 | list_value = [ 224 | {'id': '101', 'hypervisor_hostname': 'host-1'}, 225 | {'id': '201', 'hypervisor_hostname': 'host-2'}, 226 | ] 227 | delete_host, host_manager = self.create_delete_command(list_value) 228 | 229 | args = argparse.Namespace(id='host-1') 230 | delete_host.run(args) 231 | 232 | host_manager.delete.assert_called_once_with('101') 233 | 234 | def test_delete_host_with_name_startwith_number(self): 235 | list_value = [ 236 | {'id': '101', 'hypervisor_hostname': '1-host'}, 237 | {'id': '201', 'hypervisor_hostname': '2-host'}, 238 | ] 239 | delete_host, host_manager = self.create_delete_command(list_value) 240 | 241 | args = argparse.Namespace(id='1-host') 242 | delete_host.run(args) 243 | 244 | host_manager.delete.assert_called_once_with('101') 245 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /blazarclient/command.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | import ast 16 | import logging 17 | 18 | from cliff import command 19 | from cliff.formatters import table 20 | from cliff import lister 21 | from cliff import show 22 | 23 | from blazarclient import exception 24 | from blazarclient import utils 25 | 26 | HEX_ELEM = '[0-9A-Fa-f]' 27 | UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}', 28 | HEX_ELEM + '{4}', HEX_ELEM + '{4}', 29 | HEX_ELEM + '{12}']) 30 | 31 | 32 | class OpenStackCommand(command.Command): 33 | """Base class for OpenStack commands.""" 34 | 35 | api = None 36 | 37 | def run(self, parsed_args): 38 | if not self.api: 39 | return 40 | else: 41 | return super(OpenStackCommand, self).run(parsed_args) 42 | 43 | def get_data(self, parsed_args): 44 | pass 45 | 46 | def take_action(self, parsed_args): 47 | return self.get_data(parsed_args) 48 | 49 | 50 | class TableFormatter(table.TableFormatter): 51 | """This class is used to keep consistency with prettytable 0.6.""" 52 | 53 | def emit_list(self, column_names, data, stdout, parsed_args): 54 | if column_names: 55 | super(TableFormatter, self).emit_list(column_names, data, stdout, 56 | parsed_args) 57 | else: 58 | stdout.write('\n') 59 | 60 | 61 | class BlazarCommand(OpenStackCommand): 62 | 63 | """Base Blazar CLI command.""" 64 | api = 'reservation' 65 | log = logging.getLogger(__name__ + '.BlazarCommand') 66 | values_specs = [] 67 | json_indent = None 68 | resource = None 69 | allow_names = True 70 | name_key = None 71 | id_pattern = UUID_PATTERN 72 | 73 | def __init__(self, app, app_args): 74 | super(BlazarCommand, self).__init__(app, app_args) 75 | 76 | # NOTE(dbelova): This is no longer supported in cliff version 1.5.2 77 | # the same moment occurred in Neutron: 78 | # see https://bugs.launchpad.net/python-neutronclient/+bug/1265926 79 | 80 | # if hasattr(self, 'formatters'): 81 | # self.formatters['table'] = TableFormatter() 82 | 83 | def get_client(self): 84 | # client_manager.reservation is used for osc_lib, and should be used 85 | # if it exists 86 | if hasattr(self.app, 'client_manager'): 87 | return self.app.client_manager.reservation 88 | else: 89 | return self.app.client 90 | 91 | def get_parser(self, prog_name): 92 | parser = super(BlazarCommand, self).get_parser(prog_name) 93 | return parser 94 | 95 | def format_output_data(self, data): 96 | for k, v in data.items(): 97 | if isinstance(v, str): 98 | try: 99 | # Deserialize if possible into dict, lists, tuples... 100 | v = ast.literal_eval(v) 101 | except SyntaxError: 102 | # NOTE(sbauza): This is probably a datetime string, we need 103 | # to keep it unchanged. 104 | pass 105 | except ValueError: 106 | # NOTE(sbauza): This is not something AST can evaluate, 107 | # probably a string. 108 | pass 109 | if isinstance(v, list): 110 | value = '\n'.join(utils.dumps( 111 | i, indent=self.json_indent) if isinstance(i, dict) 112 | else str(i) for i in v) 113 | data[k] = value 114 | elif isinstance(v, dict): 115 | value = utils.dumps(v, indent=self.json_indent) 116 | data[k] = value 117 | elif v is None: 118 | data[k] = '' 119 | 120 | def add_known_arguments(self, parser): 121 | pass 122 | 123 | def args2body(self, parsed_args): 124 | return {} 125 | 126 | 127 | class CreateCommand(BlazarCommand, show.ShowOne): 128 | """Create resource with passed args.""" 129 | 130 | api = 'reservation' 131 | resource = None 132 | log = None 133 | 134 | def get_data(self, parsed_args): 135 | self.log.debug('get_data(%s)' % parsed_args) 136 | blazar_client = self.get_client() 137 | body = self.args2body(parsed_args) 138 | resource_manager = getattr(blazar_client, self.resource) 139 | data = resource_manager.create(**body) 140 | self.format_output_data(data) 141 | 142 | if data: 143 | print('Created a new %s:' % self.resource, file=self.app.stdout) 144 | else: 145 | data = {'': ''} 146 | return list(zip(*sorted(data.items()))) 147 | 148 | 149 | class UpdateCommand(BlazarCommand): 150 | """Update resource's information.""" 151 | 152 | api = 'reservation' 153 | resource = None 154 | log = None 155 | 156 | def get_parser(self, prog_name): 157 | parser = super(UpdateCommand, self).get_parser(prog_name) 158 | if self.allow_names: 159 | help_str = 'ID or name of %s to update' 160 | else: 161 | help_str = 'ID of %s to update' 162 | parser.add_argument( 163 | 'id', metavar=self.resource.upper(), 164 | help=help_str % self.resource 165 | ) 166 | self.add_known_arguments(parser) 167 | return parser 168 | 169 | def run(self, parsed_args): 170 | self.log.debug('run(%s)' % parsed_args) 171 | blazar_client = self.get_client() 172 | body = self.args2body(parsed_args) 173 | if self.allow_names: 174 | res_id = utils.find_resource_id_by_name_or_id(blazar_client, 175 | self.resource, 176 | parsed_args.id, 177 | self.name_key, 178 | self.id_pattern) 179 | else: 180 | res_id = parsed_args.id 181 | resource_manager = getattr(blazar_client, self.resource) 182 | resource_manager.update(res_id, **body) 183 | print('Updated %s: %s' % (self.resource, parsed_args.id), 184 | file=self.app.stdout) 185 | return 186 | 187 | 188 | class DeleteCommand(BlazarCommand): 189 | """Delete a given resource.""" 190 | 191 | api = 'reservation' 192 | resource = None 193 | log = None 194 | 195 | def get_parser(self, prog_name): 196 | parser = super(DeleteCommand, self).get_parser(prog_name) 197 | if self.allow_names: 198 | help_str = 'ID or name of %s to delete' 199 | else: 200 | help_str = 'ID of %s to delete' 201 | parser.add_argument( 202 | 'id', metavar=self.resource.upper(), 203 | help=help_str % self.resource) 204 | return parser 205 | 206 | def run(self, parsed_args): 207 | self.log.debug('run(%s)' % parsed_args) 208 | blazar_client = self.get_client() 209 | resource_manager = getattr(blazar_client, self.resource) 210 | if self.allow_names: 211 | res_id = utils.find_resource_id_by_name_or_id(blazar_client, 212 | self.resource, 213 | parsed_args.id, 214 | self.name_key, 215 | self.id_pattern) 216 | else: 217 | res_id = parsed_args.id 218 | resource_manager.delete(res_id) 219 | print('Deleted %s: %s' % (self.resource, parsed_args.id), 220 | file=self.app.stdout) 221 | return 222 | 223 | 224 | class ListCommand(BlazarCommand, lister.Lister): 225 | """List resources that belong to a given tenant.""" 226 | 227 | api = 'reservation' 228 | resource = None 229 | log = None 230 | _formatters = {} 231 | list_columns = [] 232 | unknown_parts_flag = True 233 | 234 | def args2body(self, parsed_args): 235 | params = {} 236 | if parsed_args.sort_by: 237 | if parsed_args.sort_by in self.list_columns: 238 | params['sort_by'] = parsed_args.sort_by 239 | else: 240 | msg = 'Invalid sort option %s' % parsed_args.sort_by 241 | raise exception.BlazarClientException(msg) 242 | return params 243 | 244 | def get_parser(self, prog_name): 245 | parser = super(ListCommand, self).get_parser(prog_name) 246 | return parser 247 | 248 | def retrieve_list(self, parsed_args): 249 | """Retrieve a list of resources from Blazar server.""" 250 | blazar_client = self.get_client() 251 | body = self.args2body(parsed_args) 252 | resource_manager = getattr(blazar_client, self.resource) 253 | data = resource_manager.list(**body) 254 | return data 255 | 256 | def setup_columns(self, info, parsed_args): 257 | columns = len(info) > 0 and sorted(info[0].keys()) or [] 258 | if not columns: 259 | parsed_args.columns = [] 260 | elif parsed_args.columns: 261 | columns = [col for col in parsed_args.columns if col in columns] 262 | elif self.list_columns: 263 | columns = [col for col in self.list_columns if col in columns] 264 | return ( 265 | columns, 266 | (utils.get_item_properties(s, columns, formatters=self._formatters) 267 | for s in info) 268 | ) 269 | 270 | def get_data(self, parsed_args): 271 | self.log.debug('get_data(%s)' % parsed_args) 272 | data = self.retrieve_list(parsed_args) 273 | return self.setup_columns(data, parsed_args) 274 | 275 | 276 | class ShowCommand(BlazarCommand, show.ShowOne): 277 | """Show information of a given resource.""" 278 | 279 | api = 'reservation' 280 | resource = None 281 | log = None 282 | 283 | def get_parser(self, prog_name): 284 | parser = super(ShowCommand, self).get_parser(prog_name) 285 | return parser 286 | 287 | def get_data(self, parsed_args): 288 | self.log.debug('get_data(%s)' % parsed_args) 289 | blazar_client = self.get_client() 290 | 291 | if self.allow_names: 292 | res_id = utils.find_resource_id_by_name_or_id(blazar_client, 293 | self.resource, 294 | parsed_args.id, 295 | self.name_key, 296 | self.id_pattern) 297 | else: 298 | res_id = parsed_args.id 299 | 300 | resource_manager = getattr(blazar_client, self.resource) 301 | data = resource_manager.get(res_id) 302 | self.format_output_data(data) 303 | return list(zip(*sorted(data.items()))) 304 | 305 | 306 | class ShowPropertyCommand(BlazarCommand, show.ShowOne): 307 | """Show information of a given resource property.""" 308 | 309 | api = 'reservation' 310 | resource = None 311 | log = None 312 | 313 | def get_parser(self, prog_name): 314 | parser = super(ShowPropertyCommand, self).get_parser(prog_name) 315 | parser.add_argument('property_name', metavar='PROPERTY_NAME', 316 | help='Name of property.') 317 | return parser 318 | 319 | def get_data(self, parsed_args): 320 | self.log.debug('get_data(%s)' % parsed_args) 321 | blazar_client = self.get_client() 322 | resource_manager = getattr(blazar_client, self.resource) 323 | data = resource_manager.get_property(parsed_args.property_name) 324 | if parsed_args.formatter == 'table': 325 | self.format_output_data(data) 326 | return list(zip(*sorted(data.items()))) 327 | 328 | 329 | class UpdatePropertyCommand(BlazarCommand): 330 | api = 'reservation' 331 | resource = None 332 | log = None 333 | 334 | def run(self, parsed_args): 335 | self.log.debug('run(%s)' % parsed_args) 336 | blazar_client = self.get_client() 337 | body = self.args2body(parsed_args) 338 | resource_manager = getattr(blazar_client, self.resource) 339 | resource_manager.set_property(**body) 340 | print( 341 | 'Updated %s property: %s' % ( 342 | self.resource, parsed_args.property_name), 343 | file=self.app.stdout) 344 | return 345 | 346 | def get_parser(self, prog_name): 347 | parser = super(UpdatePropertyCommand, self).get_parser(prog_name) 348 | parser.add_argument( 349 | 'property_name', metavar='PROPERTY_NAME', 350 | help='Name of property to patch.' 351 | ) 352 | parser.add_argument( 353 | '--private', 354 | action='store_true', 355 | default=False, 356 | help='Set property to private.' 357 | ) 358 | parser.add_argument( 359 | '--public', 360 | action='store_true', 361 | default=False, 362 | help='Set property to public.' 363 | ) 364 | return parser 365 | 366 | def args2body(self, parsed_args): 367 | return dict( 368 | property_name=parsed_args.property_name, 369 | private=(parsed_args.private is True)) 370 | -------------------------------------------------------------------------------- /blazarclient/tests/v1/shell_commands/test_leases.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 NTT Corp. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import argparse 17 | from datetime import datetime 18 | from unittest import mock 19 | 20 | from blazarclient import exception 21 | from blazarclient import shell 22 | from blazarclient import tests 23 | from blazarclient.v1.shell_commands import leases 24 | 25 | mock_time = mock.Mock(return_value=datetime(2020, 6, 8)) 26 | 27 | FIRST_LEASE = 'd1e43d6d-8f6f-4c2e-b0a9-2982b39dc698' 28 | SECOND_LEASE = '424d21c3-45a2-448a-81ad-32eddc888375' 29 | 30 | 31 | @mock.patch('oslo_utils.timeutils.utcnow', mock_time) 32 | class CreateLeaseTestCase(tests.TestCase): 33 | 34 | def setUp(self): 35 | super(CreateLeaseTestCase, self).setUp() 36 | self.cl = leases.CreateLease(shell.BlazarShell(), mock.Mock()) 37 | 38 | def test_args2body_correct_phys_res_params(self): 39 | args = argparse.Namespace( 40 | start='2020-07-24 20:00', 41 | end='2020-08-09 22:30', 42 | before_end='2020-08-09 21:30', 43 | events=[], 44 | name='lease-test', 45 | reservations=[], 46 | physical_reservations=[ 47 | 'min=1,' 48 | 'max=2,' 49 | 'hypervisor_properties=' 50 | '["and", [">=", "$vcpus", "2"], ' 51 | '[">=", "$memory_mb", "2048"]],' 52 | 'resource_properties=' 53 | '["==", "$extra_key", "extra_value"],' 54 | 'before_end=default' 55 | ] 56 | ) 57 | expected = { 58 | 'start': '2020-07-24 20:00', 59 | 'end': '2020-08-09 22:30', 60 | 'before_end': '2020-08-09 21:30', 61 | 'events': [], 62 | 'name': 'lease-test', 63 | 'reservations': [ 64 | { 65 | 'min': 1, 66 | 'max': 2, 67 | 'hypervisor_properties': 68 | '["and", [">=", "$vcpus", "2"], ' 69 | '[">=", "$memory_mb", "2048"]]', 70 | 'resource_properties': 71 | '["==", "$extra_key", "extra_value"]', 72 | 'resource_type': 'physical:host', 73 | 'before_end': 'default' 74 | } 75 | ] 76 | } 77 | self.assertDictEqual(self.cl.args2body(args), expected) 78 | 79 | def test_args2body_incorrect_phys_res_params(self): 80 | args = argparse.Namespace( 81 | start='2020-07-24 20:00', 82 | end='2020-08-09 22:30', 83 | before_end='2020-08-09 21:30', 84 | events=[], 85 | name='lease-test', 86 | reservations=[], 87 | physical_reservations=[ 88 | 'incorrect_param=1,' 89 | 'min=1,' 90 | 'max=2,' 91 | 'hypervisor_properties=' 92 | '["and", [">=", "$vcpus", "2"], ' 93 | '[">=", "$memory_mb", "2048"]],' 94 | 'resource_properties=' 95 | '["==", "$extra_key", "extra_value"]' 96 | ] 97 | ) 98 | self.assertRaises(exception.IncorrectLease, 99 | self.cl.args2body, 100 | args) 101 | 102 | def test_args2body_duplicated_phys_res_params(self): 103 | args = argparse.Namespace( 104 | start='2020-07-24 20:00', 105 | end='2020-08-09 22:30', 106 | before_end='2020-08-09 21:30', 107 | events=[], 108 | name='lease-test', 109 | reservations=[], 110 | physical_reservations=[ 111 | 'min=1,' 112 | 'min=1,' 113 | 'max=2,' 114 | 'hypervisor_properties=' 115 | '["and", [">=", "$vcpus", "2"], ' 116 | '[">=", "$memory_mb", "2048"]],' 117 | 'resource_properties=' 118 | '["==", "$extra_key", "extra_value"]' 119 | ] 120 | ) 121 | self.assertRaises(exception.DuplicatedLeaseParameters, 122 | self.cl.args2body, 123 | args) 124 | 125 | def test_args2body_correct_instance_res_params(self): 126 | args = argparse.Namespace( 127 | start='2020-07-24 20:00', 128 | end='2020-08-09 22:30', 129 | before_end='2020-08-09 21:30', 130 | events=[], 131 | name='lease-test', 132 | reservations=[ 133 | 'vcpus=4,' 134 | 'memory_mb=1024,' 135 | 'disk_gb=10,' 136 | 'amount=2,' 137 | 'affinity=True,' 138 | 'resource_properties=' 139 | '["==", "$extra_key", "extra_value"],' 140 | 'resource_type=virtual:instance' 141 | ], 142 | physical_reservations=[ 143 | 'min=1,' 144 | 'max=2,' 145 | 'hypervisor_properties=' 146 | '["and", [">=", "$vcpus", "2"], ' 147 | '[">=", "$memory_mb", "2048"]],' 148 | 'resource_properties=' 149 | '["==", "$extra_key", "extra_value"],' 150 | 'before_end=default' 151 | ] 152 | ) 153 | expected = { 154 | 'start': '2020-07-24 20:00', 155 | 'end': '2020-08-09 22:30', 156 | 'before_end': '2020-08-09 21:30', 157 | 'events': [], 158 | 'name': 'lease-test', 159 | 'reservations': [ 160 | { 161 | 'min': 1, 162 | 'max': 2, 163 | 'hypervisor_properties': 164 | '["and", [">=", "$vcpus", "2"], ' 165 | '[">=", "$memory_mb", "2048"]]', 166 | 'resource_properties': 167 | '["==", "$extra_key", "extra_value"]', 168 | 'resource_type': 'physical:host', 169 | 'before_end': 'default' 170 | }, 171 | { 172 | 'vcpus': 4, 173 | 'memory_mb': 1024, 174 | 'disk_gb': 10, 175 | 'amount': 2, 176 | 'affinity': 'True', 177 | 'resource_properties': 178 | '["==", "$extra_key", "extra_value"]', 179 | 'resource_type': 'virtual:instance' 180 | } 181 | ] 182 | } 183 | self.assertDictEqual(self.cl.args2body(args), expected) 184 | 185 | def test_args2body_start_now(self): 186 | args = argparse.Namespace( 187 | start='now', 188 | end='2030-08-09 22:30', 189 | before_end='2030-08-09 21:30', 190 | events=[], 191 | name='lease-test', 192 | reservations=[], 193 | physical_reservations=[ 194 | 'min=1,' 195 | 'max=2,' 196 | 'hypervisor_properties=' 197 | '["and", [">=", "$vcpus", "2"], ' 198 | '[">=", "$memory_mb", "2048"]],' 199 | 'resource_properties=' 200 | '["==", "$extra_key", "extra_value"],' 201 | 'before_end=default' 202 | ] 203 | ) 204 | expected = { 205 | 'start': 'now', 206 | 'end': '2030-08-09 22:30', 207 | 'before_end': '2030-08-09 21:30', 208 | 'events': [], 209 | 'name': 'lease-test', 210 | 'reservations': [ 211 | { 212 | 'min': 1, 213 | 'max': 2, 214 | 'hypervisor_properties': 215 | '["and", [">=", "$vcpus", "2"], ' 216 | '[">=", "$memory_mb", "2048"]]', 217 | 'resource_properties': 218 | '["==", "$extra_key", "extra_value"]', 219 | 'resource_type': 'physical:host', 220 | 'before_end': 'default' 221 | } 222 | ] 223 | } 224 | self.assertDictEqual(self.cl.args2body(args), expected) 225 | 226 | 227 | class UpdateLeaseTestCase(tests.TestCase): 228 | 229 | def setUp(self): 230 | super(UpdateLeaseTestCase, self).setUp() 231 | self.cl = leases.UpdateLease(shell.BlazarShell(), mock.Mock()) 232 | 233 | def test_args2body_time_params(self): 234 | args = argparse.Namespace( 235 | name=None, 236 | prolong_for='1h', 237 | reduce_by=None, 238 | end_date=None, 239 | defer_by=None, 240 | advance_by=None, 241 | start_date=None, 242 | reservation=None 243 | ) 244 | expected = { 245 | 'prolong_for': '1h', 246 | } 247 | 248 | self.assertDictEqual(self.cl.args2body(args), expected) 249 | 250 | def test_args2body_host_reservation_params(self): 251 | args = argparse.Namespace( 252 | name=None, 253 | prolong_for=None, 254 | reduce_by=None, 255 | end_date=None, 256 | defer_by=None, 257 | advance_by=None, 258 | start_date=None, 259 | reservation=[ 260 | 'id=798379a6-194c-45dc-ba34-1b5171d5552f,' 261 | 'max=3,' 262 | 'hypervisor_properties=' 263 | '["and", [">=", "$vcpus", "4"], ' 264 | '[">=", "$memory_mb", "8192"]],' 265 | 'resource_properties=' 266 | '["==", "$extra_key", "extra_value"]' 267 | ] 268 | ) 269 | expected = { 270 | 'reservations': [ 271 | { 272 | 'id': '798379a6-194c-45dc-ba34-1b5171d5552f', 273 | 'max': 3, 274 | 'hypervisor_properties': 275 | '["and", [">=", "$vcpus", "4"], ' 276 | '[">=", "$memory_mb", "8192"]]', 277 | 'resource_properties': 278 | '["==", "$extra_key", "extra_value"]' 279 | } 280 | ] 281 | } 282 | 283 | self.assertDictEqual(self.cl.args2body(args), expected) 284 | 285 | def test_args2body_instance_reservation_params(self): 286 | args = argparse.Namespace( 287 | name=None, 288 | prolong_for=None, 289 | reduce_by=None, 290 | end_date=None, 291 | defer_by=None, 292 | advance_by=None, 293 | start_date=None, 294 | reservation=[ 295 | 'id=798379a6-194c-45dc-ba34-1b5171d5552f,' 296 | 'vcpus=3,memory_mb=1024,disk_gb=20,' 297 | 'amount=4,affinity=False' 298 | ] 299 | ) 300 | expected = { 301 | 'reservations': [ 302 | { 303 | 'id': '798379a6-194c-45dc-ba34-1b5171d5552f', 304 | 'vcpus': 3, 305 | 'memory_mb': 1024, 306 | 'disk_gb': 20, 307 | 'amount': 4, 308 | 'affinity': 'False' 309 | } 310 | ] 311 | } 312 | 313 | self.assertDictEqual(self.cl.args2body(args), expected) 314 | 315 | 316 | class ShowLeaseTestCase(tests.TestCase): 317 | 318 | def create_show_command(self): 319 | mock_lease_manager = mock.Mock() 320 | mock_client = mock.Mock() 321 | mock_client.lease = mock_lease_manager 322 | 323 | blazar_shell = shell.BlazarShell() 324 | blazar_shell.client = mock_client 325 | return (leases.ShowLease(blazar_shell, mock.Mock()), 326 | mock_lease_manager) 327 | 328 | def test_show_lease(self): 329 | show_lease, lease_manager = self.create_show_command() 330 | lease_manager.get.return_value = {'id': FIRST_LEASE} 331 | 332 | args = argparse.Namespace(id=FIRST_LEASE) 333 | expected = [('id',), (FIRST_LEASE,)] 334 | 335 | self.assertEqual(show_lease.get_data(args), expected) 336 | lease_manager.get.assert_called_once_with(FIRST_LEASE) 337 | 338 | def test_show_lease_by_name(self): 339 | show_lease, lease_manager = self.create_show_command() 340 | lease_manager.list.return_value = [ 341 | {'id': FIRST_LEASE, 'name': 'first-lease'}, 342 | {'id': SECOND_LEASE, 'name': 'second-lease'}, 343 | ] 344 | lease_manager.get.return_value = {'id': SECOND_LEASE} 345 | 346 | args = argparse.Namespace(id='second-lease') 347 | expected = [('id',), (SECOND_LEASE,)] 348 | 349 | self.assertEqual(show_lease.get_data(args), expected) 350 | lease_manager.list.assert_called_once_with() 351 | lease_manager.get.assert_called_once_with(SECOND_LEASE) 352 | 353 | 354 | class DeleteLeaseTestCase(tests.TestCase): 355 | 356 | def create_delete_command(self): 357 | mock_lease_manager = mock.Mock() 358 | mock_client = mock.Mock() 359 | mock_client.lease = mock_lease_manager 360 | 361 | blazar_shell = shell.BlazarShell() 362 | blazar_shell.client = mock_client 363 | return (leases.DeleteLease(blazar_shell, mock.Mock()), 364 | mock_lease_manager) 365 | 366 | def test_delete_lease(self): 367 | delete_lease, lease_manager = self.create_delete_command() 368 | lease_manager.delete.return_value = None 369 | 370 | args = argparse.Namespace(id=FIRST_LEASE) 371 | delete_lease.run(args) 372 | 373 | lease_manager.delete.assert_called_once_with(FIRST_LEASE) 374 | 375 | def test_delete_lease_by_name(self): 376 | delete_lease, lease_manager = self.create_delete_command() 377 | lease_manager.list.return_value = [ 378 | {'id': FIRST_LEASE, 'name': 'first-lease'}, 379 | {'id': SECOND_LEASE, 'name': 'second-lease'}, 380 | ] 381 | lease_manager.delete.return_value = None 382 | 383 | args = argparse.Namespace(id='second-lease') 384 | delete_lease.run(args) 385 | 386 | lease_manager.list.assert_called_once_with() 387 | lease_manager.delete.assert_called_once_with(SECOND_LEASE) 388 | -------------------------------------------------------------------------------- /blazarclient/shell.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | """ 17 | Command-line interface to the Blazar APIs 18 | """ 19 | import argparse 20 | import logging 21 | import os 22 | import sys 23 | 24 | from cliff import app 25 | from cliff import commandmanager 26 | from keystoneauth1 import loading 27 | from oslo_utils import encodeutils 28 | 29 | from blazarclient import client as blazar_client 30 | from blazarclient import exception 31 | from blazarclient.v1.shell_commands import allocations 32 | from blazarclient.v1.shell_commands import floatingips 33 | from blazarclient.v1.shell_commands import hosts 34 | from blazarclient.v1.shell_commands import leases 35 | from blazarclient import version as base_version 36 | 37 | COMMANDS_V1 = { 38 | 'lease-list': leases.ListLeases, 39 | 'lease-show': leases.ShowLease, 40 | 'lease-create': leases.CreateLease, 41 | 'lease-update': leases.UpdateLease, 42 | 'lease-delete': leases.DeleteLease, 43 | 'host-list': hosts.ListHosts, 44 | 'host-show': hosts.ShowHost, 45 | 'host-create': hosts.CreateHost, 46 | 'host-update': hosts.UpdateHost, 47 | 'host-delete': hosts.DeleteHost, 48 | 'host-property-list': hosts.ListHostProperties, 49 | 'host-property-show': hosts.ShowHostProperty, 50 | 'host-property-set': hosts.UpdateHostProperty, 51 | 'floatingip-list': floatingips.ListFloatingIPs, 52 | 'floatingip-show': floatingips.ShowFloatingIP, 53 | 'floatingip-create': floatingips.CreateFloatingIP, 54 | 'floatingip-delete': floatingips.DeleteFloatingIP, 55 | 'allocation-list': allocations.ListAllocations, 56 | 'allocation-show': allocations.ShowAllocations, 57 | } 58 | 59 | VERSION = 1 60 | DEFAULT_API_VERSION = 1 61 | COMMANDS = {'v1': COMMANDS_V1} 62 | 63 | 64 | def run_command(cmd, cmd_parser, sub_argv): 65 | _argv = sub_argv 66 | index = -1 67 | values_specs = [] 68 | if '--' in sub_argv: 69 | index = sub_argv.index('--') 70 | _argv = sub_argv[:index] 71 | values_specs = sub_argv[index:] 72 | known_args, _values_specs = cmd_parser.parse_known_args(_argv) 73 | cmd.values_specs = (index == -1 and _values_specs or values_specs) 74 | return cmd.run(known_args) 75 | 76 | 77 | def env(*_vars, **kwargs): 78 | """Search for the first defined of possibly many env vars. 79 | 80 | Returns the first environment variable defined in vars, or 81 | returns the default defined in kwargs. 82 | 83 | """ 84 | for v in _vars: 85 | value = os.environ.get(v, None) 86 | if value: 87 | return value 88 | return kwargs.get('default', '') 89 | 90 | 91 | class HelpAction(argparse.Action): 92 | """Provide a custom action so the -h and --help options 93 | to the main app will print a list of the commands. 94 | 95 | The commands are determined by checking the CommandManager 96 | instance, passed in as the "default" value for the action. 97 | """ 98 | def __call__(self, parser, namespace, values, option_string=None): 99 | outputs = [] 100 | max_len = 0 101 | app = self.default 102 | parser.print_help(app.stdout) 103 | app.stdout.write('\nCommands for API %s:\n' % app.api_version) 104 | command_manager = app.command_manager 105 | for name, ep in sorted(command_manager): 106 | factory = ep.load() 107 | cmd = factory(self, None) 108 | one_liner = cmd.get_description().split('\n')[0] 109 | outputs.append((name, one_liner)) 110 | max_len = max(len(name), max_len) 111 | for (name, one_liner) in outputs: 112 | app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) 113 | sys.exit(0) 114 | 115 | 116 | class BlazarShell(app.App): 117 | """Manager class for the Blazar CLI.""" 118 | CONSOLE_MESSAGE_FORMAT = '%(message)s' 119 | DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' 120 | log = logging.getLogger(__name__) 121 | 122 | def __init__(self): 123 | super(BlazarShell, self).__init__( 124 | description=__doc__.strip(), 125 | version=VERSION, 126 | command_manager=commandmanager.CommandManager('blazar.cli'), ) 127 | self.commands = COMMANDS 128 | 129 | def build_option_parser(self, description, version, argparse_kwargs=None): 130 | """Return an argparse option parser for this application. 131 | 132 | Subclasses may override this method to extend 133 | the parser with more global options. 134 | """ 135 | parser = argparse.ArgumentParser( 136 | description=description, 137 | add_help=False) 138 | parser.add_argument( 139 | '--version', 140 | action='version', 141 | version=base_version.__version__) 142 | parser.add_argument( 143 | '-v', '--verbose', 144 | action='count', 145 | dest='verbose_level', 146 | default=self.DEFAULT_VERBOSE_LEVEL, 147 | help='Increase verbosity of output. Can be repeated.') 148 | parser.add_argument( 149 | '-q', '--quiet', 150 | action='store_const', 151 | dest='verbose_level', 152 | const=0, 153 | help='suppress output except warnings and errors') 154 | help_action = parser.add_argument( 155 | '-h', '--help', 156 | action=HelpAction, 157 | nargs=0, 158 | default=self, 159 | help="show this help message and exit") 160 | parser.add_argument( 161 | '--debug', 162 | default=False, 163 | action='store_true', 164 | help='Print debugging output') 165 | 166 | # Removes help action to defer its execution 167 | self.deferred_help_action = help_action 168 | parser._actions.remove(help_action) 169 | del parser._option_string_actions['-h'] 170 | del parser._option_string_actions['--help'] 171 | parser.add_argument( 172 | '-h', '--help', 173 | action='store_true', 174 | dest='deferred_help', 175 | default=False, 176 | help="Show this help message and exit", 177 | ) 178 | 179 | # Global arguments 180 | parser.add_argument( 181 | '--os-reservation-api-version', 182 | default=env('OS_RESERVATION_API_VERSION', 183 | default=DEFAULT_API_VERSION), 184 | help='Accepts 1 now, defaults to 1.') 185 | parser.add_argument( 186 | '--os_reservation_api_version', 187 | help=argparse.SUPPRESS) 188 | 189 | # Deprecated arguments 190 | parser.add_argument( 191 | '--service-type', metavar='', 192 | default=env('BLAZAR_SERVICE_TYPE'), 193 | help=('(deprecated) Use --os-service-type instead. ' 194 | 'Defaults to env[BLAZAR_SERVICE_TYPE].')) 195 | parser.add_argument( 196 | '--endpoint-type', metavar='', 197 | default=env('OS_ENDPOINT_TYPE'), 198 | help=('(deprecated) Use --os-interface instead. ' 199 | 'Defaults to env[OS_ENDPOINT_TYPE].')) 200 | 201 | return parser 202 | 203 | def _bash_completion(self): 204 | """Prints all of the commands and options for bash-completion.""" 205 | commands = set() 206 | options = set() 207 | 208 | for option, _action in self.parser._option_string_actions.items(): 209 | options.add(option) 210 | 211 | for command_name, command in self.command_manager: 212 | commands.add(command_name) 213 | cmd_factory = command.load() 214 | cmd = cmd_factory(self, None) 215 | cmd_parser = cmd.get_parser('') 216 | for option, _action in cmd_parser._option_string_actions.items(): 217 | options.add(option) 218 | 219 | print(' '.join(commands | options)) 220 | 221 | def run(self, argv): 222 | """Equivalent to the main program for the application. 223 | 224 | :param argv: input arguments and options 225 | :paramtype argv: list of str 226 | """ 227 | loading.register_auth_argparse_arguments(self.parser, argv) 228 | loading.session.register_argparse_arguments(self.parser) 229 | loading.adapter.register_argparse_arguments( 230 | self.parser, service_type='reservation') 231 | 232 | try: 233 | self.options, remainder = self.parser.parse_known_args(argv) 234 | 235 | self.api_version = 'v%s' % self.options.os_reservation_api_version 236 | for k, v in self.commands[self.api_version].items(): 237 | self.command_manager.add_command(k, v) 238 | 239 | index = 0 240 | command_pos = -1 241 | help_pos = -1 242 | help_command_pos = -1 243 | 244 | for arg in argv: 245 | if arg == 'bash-completion': 246 | self._bash_completion() 247 | return 0 248 | if arg in self.commands[self.api_version]: 249 | if command_pos == -1: 250 | command_pos = index 251 | elif arg in ('-h', '--help'): 252 | if help_pos == -1: 253 | help_pos = index 254 | elif arg == 'help': 255 | if help_command_pos == -1: 256 | help_command_pos = index 257 | index += 1 258 | 259 | if -1 < command_pos < help_pos: 260 | argv = ['help', argv[command_pos]] 261 | if help_command_pos > -1 and command_pos == -1: 262 | argv[help_command_pos] = '--help' 263 | 264 | if self.options.deferred_help: 265 | self.deferred_help_action(self.parser, self.parser, None, None) 266 | 267 | self.configure_logging() 268 | self.interactive_mode = not remainder 269 | self.initialize_app(remainder) 270 | 271 | except Exception as err: 272 | if self.options.debug: 273 | self.log.exception(str(err)) 274 | raise 275 | else: 276 | self.log.error(str(err)) 277 | return 1 278 | if self.interactive_mode: 279 | _argv = [sys.argv[0]] 280 | sys.argv = _argv 281 | result = self.interact() 282 | else: 283 | result = self.run_subcommand(remainder) 284 | return result 285 | 286 | def run_subcommand(self, argv): 287 | subcommand = self.command_manager.find_command(argv) 288 | cmd_factory, cmd_name, sub_argv = subcommand 289 | cmd = cmd_factory(self, self.options) 290 | result = 1 291 | try: 292 | self.prepare_to_run_command(cmd) 293 | full_name = (cmd_name if self.interactive_mode else 294 | ' '.join([self.NAME, cmd_name])) 295 | cmd_parser = cmd.get_parser(full_name) 296 | return run_command(cmd, cmd_parser, sub_argv) 297 | except Exception as err: 298 | if self.options.debug: 299 | self.log.exception(str(err)) 300 | else: 301 | self.log.error(str(err)) 302 | try: 303 | self.clean_up(cmd, result, err) 304 | except Exception as err2: 305 | if self.options.debug: 306 | self.log.exception(str(err2)) 307 | else: 308 | self.log.error('Could not clean up: %s', 309 | str(err2)) 310 | if self.options.debug: 311 | raise 312 | else: 313 | try: 314 | self.clean_up(cmd, result, None) 315 | except Exception as err3: 316 | if self.options.debug: 317 | self.log.exception(str(err3)) 318 | else: 319 | self.log.error('Could not clean up: %s', 320 | str(err3)) 321 | return result 322 | 323 | def authenticate_user(self): 324 | """Authenticate user and set client by using passed params.""" 325 | auth = loading.load_auth_from_argparse_arguments(self.options) 326 | sess = loading.load_session_from_argparse_arguments( 327 | self.options, auth=auth) 328 | self.client = blazar_client.Client( 329 | self.options.os_reservation_api_version, 330 | session=sess, 331 | service_type=(self.options.service_type or 332 | self.options.os_service_type), 333 | interface=self.options.endpoint_type or self.options.os_interface, 334 | region_name=self.options.os_region_name, 335 | ) 336 | return 337 | 338 | def initialize_app(self, argv): 339 | """Global app init bits: 340 | 341 | * set up API versions 342 | * validate authentication info 343 | """ 344 | 345 | super(BlazarShell, self).initialize_app(argv) 346 | 347 | cmd_name = None 348 | if argv: 349 | cmd_info = self.command_manager.find_command(argv) 350 | cmd_factory, cmd_name, sub_argv = cmd_info 351 | if self.interactive_mode or cmd_name != 'help': 352 | self.authenticate_user() 353 | 354 | def clean_up(self, cmd, result, err): 355 | self.log.debug('clean_up %s', cmd.__class__.__name__) 356 | if err: 357 | self.log.debug('got an error: %s', str(err)) 358 | 359 | def configure_logging(self): 360 | """Create logging handlers for any log output.""" 361 | root_logger = logging.getLogger('') 362 | 363 | # Set up logging to a file 364 | root_logger.setLevel(logging.DEBUG) 365 | 366 | # Send higher-level messages to the console via stderr 367 | console = logging.StreamHandler(self.stderr) 368 | if self.options.debug: 369 | console_level = logging.DEBUG 370 | else: 371 | console_level = {0: logging.WARNING, 372 | 1: logging.INFO, 373 | 2: logging.DEBUG}.get(self.options.verbose_level, 374 | logging.DEBUG) 375 | console.setLevel(console_level) 376 | if logging.DEBUG == console_level: 377 | formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT) 378 | else: 379 | formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT) 380 | console.setFormatter(formatter) 381 | root_logger.addHandler(console) 382 | return 383 | 384 | 385 | def main(argv=sys.argv[1:]): 386 | try: 387 | return BlazarShell().run(list(map(encodeutils.safe_decode, argv))) 388 | except exception.BlazarClientException: 389 | return 1 390 | except Exception as e: 391 | print(str(e)) 392 | return 1 393 | 394 | 395 | if __name__ == "__main__": 396 | sys.exit(main(sys.argv[1:])) 397 | -------------------------------------------------------------------------------- /blazarclient/v1/shell_commands/leases.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Mirantis Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import argparse 17 | import datetime 18 | import logging 19 | import re 20 | 21 | from oslo_serialization import jsonutils 22 | from oslo_utils import strutils 23 | from oslo_utils import timeutils 24 | 25 | from blazarclient import command 26 | from blazarclient import exception 27 | 28 | 29 | # All valid reservation parameters must be added to CREATE_RESERVATION_KEYS to 30 | # make them parsable. Note that setting the default value to None ensures that 31 | # the parameter is not included in the POST request if absent. 32 | CREATE_RESERVATION_KEYS = { 33 | "physical:host": { 34 | "min": "", 35 | "max": "", 36 | "hypervisor_properties": "", 37 | "resource_properties": "", 38 | "before_end": None, 39 | "resource_type": 'physical:host' 40 | }, 41 | "virtual:floatingip": { 42 | "amount": 1, 43 | "network_id": None, 44 | "required_floatingips": [], 45 | "resource_type": 'virtual:floatingip' 46 | }, 47 | "virtual:instance": { 48 | "vcpus": "", 49 | "memory_mb": "", 50 | "disk_gb": "", 51 | "amount": "", 52 | "affinity": "None", 53 | "resource_properties": "", 54 | "resource_type": 'virtual:instance' 55 | }, 56 | "flavor:instance": { 57 | "flavor_id": "", 58 | "amount": "", 59 | "affinity": "None", 60 | "resource_type": 'flavor:instance' 61 | }, 62 | "others": { 63 | ".*": None 64 | } 65 | } 66 | 67 | 68 | class ListLeases(command.ListCommand): 69 | """Print a list of leases.""" 70 | resource = 'lease' 71 | log = logging.getLogger(__name__ + '.ListLeases') 72 | list_columns = ['id', 'name', 'start_date', 'end_date'] 73 | 74 | def get_parser(self, prog_name): 75 | parser = super(ListLeases, self).get_parser(prog_name) 76 | parser.add_argument( 77 | '--sort-by', metavar="", 78 | help='column name used to sort result', 79 | default='name' 80 | ) 81 | return parser 82 | 83 | 84 | class ShowLease(command.ShowCommand): 85 | """Show details about the given lease.""" 86 | resource = 'lease' 87 | json_indent = 4 88 | log = logging.getLogger(__name__ + '.ShowLease') 89 | 90 | def get_parser(self, prog_name): 91 | parser = super(ShowLease, self).get_parser(prog_name) 92 | if self.allow_names: 93 | help_str = 'ID or name of %s to look up' 94 | else: 95 | help_str = 'ID of %s to look up' 96 | parser.add_argument('id', metavar=self.resource.upper(), 97 | help=help_str % self.resource) 98 | return parser 99 | 100 | 101 | class CreateLeaseBase(command.CreateCommand): 102 | """Create a lease.""" 103 | resource = 'lease' 104 | json_indent = 4 105 | log = logging.getLogger(__name__ + '.CreateLease') 106 | default_start = 'now' 107 | default_end = timeutils.utcnow() + datetime.timedelta(days=1) 108 | 109 | def get_parser(self, prog_name): 110 | parser = super(CreateLeaseBase, self).get_parser(prog_name) 111 | parser.add_argument( 112 | 'name', metavar=self.resource.upper(), 113 | help='Name for the %s' % self.resource 114 | ) 115 | parser.add_argument( 116 | '--start-date', 117 | dest='start', 118 | help='Time (YYYY-MM-DD HH:MM) UTC TZ for starting the lease ' 119 | '(default: current time on the server)', 120 | default=self.default_start 121 | ) 122 | parser.add_argument( 123 | '--end-date', 124 | dest='end', 125 | help='Time (YYYY-MM-DD HH:MM) UTC TZ for ending the lease ' 126 | '(default: 24h from now)', 127 | default=self.default_end 128 | ) 129 | parser.add_argument( 130 | '--before-end-date', 131 | dest='before_end', 132 | help='Time (YYYY-MM-DD HH:MM) UTC TZ for taking an action before ' 133 | 'the end of the lease (default: depends on system default)', 134 | default=None 135 | ) 136 | parser.add_argument( 137 | '--reservation', 138 | metavar="", 139 | action='append', 140 | dest='reservations', 141 | help='key/value pairs for creating a generic reservation. ' 142 | 'Specify option multiple times to create multiple ' 143 | 'reservations. ', 144 | default=[] 145 | ) 146 | parser.add_argument( 147 | '--event', metavar='', 148 | action='append', 149 | dest='events', 150 | help='Creates an event with key/value pairs for the lease. ' 151 | 'Specify option multiple times to create multiple events. ' 152 | 'event_type: type of event (e.g. notification). ' 153 | 'event_date: Time for event (YYYY-MM-DD HH:MM) UTC TZ. ', 154 | default=[] 155 | ) 156 | return parser 157 | 158 | def args2body(self, parsed_args): 159 | params = self._generate_params(parsed_args) 160 | if not params['reservations']: 161 | raise exception.IncorrectLease 162 | return params 163 | 164 | def _generate_params(self, parsed_args): 165 | params = {} 166 | if parsed_args.name: 167 | params['name'] = parsed_args.name 168 | if not isinstance(parsed_args.start, datetime.datetime): 169 | if parsed_args.start != 'now': 170 | try: 171 | parsed_args.start = datetime.datetime.strptime( 172 | parsed_args.start, '%Y-%m-%d %H:%M') 173 | except ValueError: 174 | raise exception.IncorrectLease 175 | if not isinstance(parsed_args.end, datetime.datetime): 176 | try: 177 | parsed_args.end = datetime.datetime.strptime( 178 | parsed_args.end, '%Y-%m-%d %H:%M') 179 | except ValueError: 180 | raise exception.IncorrectLease 181 | 182 | if parsed_args.start == 'now': 183 | start = timeutils.utcnow() 184 | else: 185 | start = parsed_args.start 186 | 187 | if start > parsed_args.end: 188 | raise exception.IncorrectLease 189 | 190 | if parsed_args.before_end: 191 | try: 192 | parsed_args.before_end = datetime.datetime.strptime( 193 | parsed_args.before_end, '%Y-%m-%d %H:%M') 194 | except ValueError: 195 | raise exception.IncorrectLease 196 | if (parsed_args.before_end < start or 197 | parsed_args.end < parsed_args.before_end): 198 | raise exception.IncorrectLease 199 | params['before_end'] = datetime.datetime.strftime( 200 | parsed_args.before_end, '%Y-%m-%d %H:%M') 201 | 202 | if parsed_args.start == 'now': 203 | params['start'] = parsed_args.start 204 | else: 205 | params['start'] = datetime.datetime.strftime(parsed_args.start, 206 | '%Y-%m-%d %H:%M') 207 | params['end'] = datetime.datetime.strftime(parsed_args.end, 208 | '%Y-%m-%d %H:%M') 209 | 210 | params['reservations'] = [] 211 | params['events'] = [] 212 | 213 | reservations = [] 214 | for res_str in parsed_args.reservations: 215 | err_msg = ("Invalid reservation argument '%s'. " 216 | "Reservation arguments must be of the " 217 | "form --reservation " 218 | % res_str) 219 | 220 | if "physical:host" in res_str: 221 | defaults = CREATE_RESERVATION_KEYS['physical:host'] 222 | elif "virtual:instance" in res_str: 223 | defaults = CREATE_RESERVATION_KEYS['virtual:instance'] 224 | elif "virtual:floatingip" in res_str: 225 | defaults = CREATE_RESERVATION_KEYS['virtual:floatingip'] 226 | elif "flavor:instance" in res_str: 227 | defaults = CREATE_RESERVATION_KEYS['flavor:instance'] 228 | else: 229 | defaults = CREATE_RESERVATION_KEYS['others'] 230 | 231 | res_info = self._parse_params(res_str, defaults, err_msg) 232 | reservations.append(res_info) 233 | 234 | if reservations: 235 | params['reservations'] += reservations 236 | 237 | events = [] 238 | for event_str in parsed_args.events: 239 | err_msg = ("Invalid event argument '%s'. " 240 | "Event arguments must be of the " 241 | "form --event " 242 | % event_str) 243 | event_info = {"event_type": "", "event_date": ""} 244 | for kv_str in event_str.split(","): 245 | try: 246 | k, v = kv_str.split("=", 1) 247 | except ValueError: 248 | raise exception.IncorrectLease(err_msg) 249 | if k in event_info: 250 | event_info[k] = v 251 | else: 252 | raise exception.IncorrectLease(err_msg) 253 | if not event_info['event_type'] and not event_info['event_date']: 254 | raise exception.IncorrectLease(err_msg) 255 | event_date = event_info['event_date'] 256 | try: 257 | date = datetime.datetime.strptime(event_date, '%Y-%m-%d %H:%M') 258 | event_date = datetime.datetime.strftime(date, '%Y-%m-%d %H:%M') 259 | event_info['event_date'] = event_date 260 | except ValueError: 261 | raise exception.IncorrectLease 262 | events.append(event_info) 263 | if events: 264 | params['events'] = events 265 | 266 | return params 267 | 268 | def _parse_params(self, str_params, default, err_msg): 269 | request_params = {} 270 | prog = re.compile('^(?:(.*),)?(%s)=(.*)$' 271 | % "|".join(default.keys())) 272 | 273 | while str_params != "": 274 | match = prog.search(str_params) 275 | 276 | if match is None: 277 | raise exception.IncorrectLease(err_msg) 278 | 279 | self.log.info("Matches: %s", match.groups()) 280 | k, v = match.group(2, 3) 281 | if k in request_params.keys(): 282 | raise exception.DuplicatedLeaseParameters(err_msg) 283 | else: 284 | if strutils.is_int_like(v): 285 | request_params[k] = int(v) 286 | elif isinstance(default[k], list): 287 | request_params[k] = jsonutils.loads(v) 288 | else: 289 | request_params[k] = v 290 | 291 | str_params = match.group(1) if match.group(1) else "" 292 | 293 | request_params.update({k: v for k, v in default.items() 294 | if k not in request_params.keys() and 295 | v is not None}) 296 | return request_params 297 | 298 | 299 | class CreateLease(CreateLeaseBase): 300 | 301 | def get_parser(self, prog_name): 302 | parser = super(CreateLease, self).get_parser(prog_name) 303 | parser.add_argument( 304 | '--physical-reservation', 305 | metavar="", 307 | action='append', 308 | dest='physical_reservations', 309 | help='Create a reservation for physical compute hosts. ' 310 | 'Specify option multiple times to create multiple ' 311 | 'reservations. ' 312 | 'min: minimum number of hosts to reserve. ' 313 | 'max: maximum number of hosts to reserve. ' 314 | 'hypervisor_properties: JSON string, see doc. ' 315 | 'resource_properties: JSON string, see doc. ' 316 | 'before_end: JSON string, see doc. ', 317 | default=[] 318 | ) 319 | return parser 320 | 321 | def args2body(self, parsed_args): 322 | params = self._generate_params(parsed_args) 323 | 324 | physical_reservations = [] 325 | for phys_res_str in parsed_args.physical_reservations: 326 | err_msg = ("Invalid physical-reservation argument '%s'. " 327 | "Reservation arguments must be of the " 328 | "form --physical-reservation " 331 | % phys_res_str) 332 | defaults = CREATE_RESERVATION_KEYS["physical:host"] 333 | phys_res_info = self._parse_params(phys_res_str, defaults, err_msg) 334 | 335 | if not (phys_res_info['min'] and phys_res_info['max']): 336 | raise exception.IncorrectLease(err_msg) 337 | 338 | if not (strutils.is_int_like(phys_res_info['min']) and 339 | strutils.is_int_like(phys_res_info['max'])): 340 | raise exception.IncorrectLease(err_msg) 341 | 342 | min_host = int(phys_res_info['min']) 343 | max_host = int(phys_res_info['max']) 344 | 345 | if min_host > max_host: 346 | err_msg = ("Invalid physical-reservation argument '%s'. " 347 | "Reservation argument min value must be " 348 | "less than max value" 349 | % phys_res_str) 350 | raise exception.IncorrectLease(err_msg) 351 | 352 | if min_host == 0 or max_host == 0: 353 | err_msg = ("Invalid physical-reservation argument '%s'. " 354 | "Reservation arguments min and max values " 355 | "must be greater than or equal to 1" 356 | % phys_res_str) 357 | raise exception.IncorrectLease(err_msg) 358 | 359 | # NOTE(sbauza): The resource type should be conf-driven mapped with 360 | # blazar.conf file but that's potentially on another 361 | # host 362 | phys_res_info['resource_type'] = 'physical:host' 363 | physical_reservations.append(phys_res_info) 364 | if physical_reservations: 365 | # We prepend the physical_reservations to preserve legacy order 366 | # of reservations 367 | params['reservations'] = physical_reservations \ 368 | + params['reservations'] 369 | 370 | return params 371 | 372 | 373 | class UpdateLease(command.UpdateCommand): 374 | """Update a lease.""" 375 | resource = 'lease' 376 | json_indent = 4 377 | log = logging.getLogger(__name__ + '.UpdateLease') 378 | 379 | def get_parser(self, prog_name): 380 | parser = super(UpdateLease, self).get_parser(prog_name) 381 | parser.add_argument( 382 | '--name', 383 | help='New name for the lease', 384 | default=None 385 | ) 386 | parser.add_argument( 387 | '--reservation', 388 | metavar="", 389 | action='append', 390 | help='Reservation values to update. The reservation must be ' 391 | 'selected with the id= key-value pair.', 392 | default=None) 393 | 394 | #prolong-for and reduce_by are mutually exclusive 395 | group = parser.add_mutually_exclusive_group() 396 | group.add_argument( 397 | '--prolong-for', 398 | help='Time to prolong lease for', 399 | default=None 400 | ) 401 | group.add_argument( 402 | '--prolong_for', 403 | help=argparse.SUPPRESS, 404 | default=None 405 | ) 406 | group.add_argument( 407 | '--reduce-by', 408 | help='Time to reduce lease by', 409 | default=None 410 | ) 411 | group.add_argument( 412 | '--end-date', 413 | help='end date of the lease', 414 | default=None) 415 | 416 | #defer-by and a 'future' advance-by are mutually exclusive 417 | group = parser.add_mutually_exclusive_group() 418 | group.add_argument( 419 | '--defer-by', 420 | help='Time to defer the lease start', 421 | default=None 422 | ) 423 | group.add_argument( 424 | '--advance-by', 425 | help='Time to advance the lease start', 426 | default=None 427 | ) 428 | group.add_argument( 429 | '--start-date', 430 | help='start date of the lease', 431 | default=None) 432 | 433 | return parser 434 | 435 | def args2body(self, parsed_args): 436 | params = {} 437 | if parsed_args.name: 438 | params['name'] = parsed_args.name 439 | if parsed_args.prolong_for: 440 | params['prolong_for'] = parsed_args.prolong_for 441 | if parsed_args.reduce_by: 442 | params['reduce_by'] = parsed_args.reduce_by 443 | if parsed_args.end_date: 444 | params['end_date'] = parsed_args.end_date 445 | if parsed_args.defer_by: 446 | params['defer_by'] = parsed_args.defer_by 447 | if parsed_args.advance_by: 448 | params['advance_by'] = parsed_args.advance_by 449 | if parsed_args.start_date: 450 | params['start_date'] = parsed_args.start_date 451 | if parsed_args.reservation: 452 | keys = set([ 453 | # General keys 454 | 'id', 455 | # Keys for host reservation 456 | 'min', 'max', 'hypervisor_properties', 'resource_properties', 457 | # Keys for instance reservation 458 | 'vcpus', 'memory_mb', 'disk_gb', 'amount', 'affinity', 459 | # Keys for floating IP reservation 460 | 'amount', 'network_id', 'required_floatingips', 461 | ]) 462 | list_keys = ['required_floatingips'] 463 | params['reservations'] = [] 464 | reservations = [] 465 | for res_str in parsed_args.reservation: 466 | err_msg = ("Invalid reservation argument '%s'. " 467 | "Reservation arguments must be of the form " 468 | "--reservation " % res_str) 469 | res_info = {} 470 | prog = re.compile('^(?:(.*),)?(%s)=(.*)$' % '|'.join(keys)) 471 | 472 | def parse_params(params): 473 | match = prog.search(params) 474 | if match: 475 | k, v = match.group(2, 3) 476 | if k in list_keys: 477 | v = jsonutils.loads(v) 478 | elif strutils.is_int_like(v): 479 | v = int(v) 480 | res_info[k] = v 481 | if match.group(1) is not None: 482 | parse_params(match.group(1)) 483 | 484 | parse_params(res_str) 485 | if res_info: 486 | if 'id' not in res_info: 487 | raise exception.IncorrectLease( 488 | 'The key-value pair id= is ' 489 | 'required for the --reservation argument') 490 | reservations.append(res_info) 491 | if not reservations: 492 | raise exception.IncorrectLease(err_msg) 493 | params['reservations'] = reservations 494 | return params 495 | 496 | 497 | class DeleteLease(command.DeleteCommand): 498 | """Delete a lease.""" 499 | resource = 'lease' 500 | log = logging.getLogger(__name__ + '.DeleteLease') 501 | --------------------------------------------------------------------------------